<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://cthoyt.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://cthoyt.com/" rel="alternate" type="text/html" /><updated>2026-06-19T13:48:53+00:00</updated><id>https://cthoyt.com/feed.xml</id><title type="html">Biopragmatics</title><subtitle>Unraveling complex biology with biological knowledge graphs. Content licensed under CC BY 4.0.
</subtitle><author><name>Charles Tapley Hoyt</name></author><entry><title type="html">Comparing manually curated semantic mappings in SSSOM</title><link href="https://cthoyt.com/2026/06/19/comparing-sssom.html" rel="alternate" type="text/html" title="Comparing manually curated semantic mappings in SSSOM" /><published>2026-06-19T12:47:00+00:00</published><updated>2026-06-19T12:47:00+00:00</updated><id>https://cthoyt.com/2026/06/19/comparing-sssom</id><content type="html" xml:base="https://cthoyt.com/2026/06/19/comparing-sssom.html"><![CDATA[<p>I am currently supporting <a href="https://github.com/StroemPhi">Philip Strömert</a> and
<a href="https://github.com/NRayya">Noura Rayya</a> in the efforts to modernize and
revitalize the <a href="https://semantic.farm/chmo">Chemical Methods Ontology (CHMO)</a> to
support annotation of instrumentation used to produce experimental data captured
in the <a href="https://chemotion.net/">Chemotion</a> electronic laboratory notebook as
part of <a href="https://nfdi4chem.de">NFDIChem</a>. This post is about the adoption of
<a href="https://mapping-commons.github.io/sssom">Simple Standard for Sharing Ontological Mappings (SSSOM)</a>
to support interoperability between CHMO and other resources, and the workflow I
developed to compare overlapping manual curations from different researchers.</p>

<p>Philip and Noura have already completed the important initial steps of assuming
maintainership from the Royal Society of Chemistry, porting the ontology to use
a standardized
<a href="https://github.com/INCATools/ontology-development-kit/">Ontology Development Kit (ODK)</a>
layout, and
<a href="https://github.com/rsc-ontology/rsc-cmo/pull/70">revising the definitions</a> of
many classes based on the <a href="https://goldbook.iupac.org">IUPAC GoldBook</a>.</p>

<h2 id="landscape-of-resources">Landscape of Resources</h2>

<p>There are several other NFDI consortia including NFDI4Cat (catalysis),
DAPHNE4NFDI (photon and neutron physics), and FAIRmat (materials science) that
have similar goals to annotate instrumentation. While each reuse CHMO to some
extent for this purpose, DAPHNE4NFDI additionally develops the
<a href="https://semantic.farm/panet">Photon and Neutron Experimental Techniques (PANET) Ontology</a>
and FAIRmat develops the <a href="https://www.nexusformat.org/">NeXus format</a> and
associated <a href="https://semantic.farm/nexus">NeXus Ontology</a> as part of the
<a href="https://nomad-lab.eu/nomad-lab/index.html">NOMAD</a> materials science data
management platform.</p>

<p>Further, there are several other resources with similar goals including the
<a href="https://www.allotrope.org/ontologieshttps://www.allotrope.org/ontologies">Allotrope Foundation Ontology (AFO)</a>,
the deprecated
<a href="https://semantic.farm/fix">Physico-chemical Methods and Properties (FIX)</a>
ontology, the deprecated
<a href="https://semantic.farm/rex">Physico-chemical process (REX)</a> ontology,
<a href="https://goldbook.iupac.org">IUPAC GoldBook</a>, and
<a href="https://wikidata.org">Wikidata</a>.</p>

<h1 id="establishing-interoperability">Establishing Interoperability</h1>

<p>In order to establish interoperability between these many resources, we are
using the
<a href="https://mapping-commons.github.io/sssom">Simple Standard for Sharing Ontological Mappings (SSSOM)</a>
to curate exact matches, narrow matches, and broad matches between CHMO terms
and external ones in PANET, NeXuS (sort of), AFO, FIX, REX, IUPAC GoldBook, and
Wikidata.</p>

<p>First, Philip had Ambika, a student research assistant (<em>Hiwi</em>, abbreviated in
German), work for several months to manually curate mappings from CHMO to REX,
FIX, AFO, and Wikidata (see
<a href="https://github.com/rsc-ontology/rsc-cmo/pull/77">this PR</a>).</p>

<p>In parallel, I took the opportunity to spin up a new instance of a
<a href="https://github.com/cthoyt/sssom-curator/">SSSOM Curator</a> repository within the
NFDI Section Metadata Working Group for Ontology Harmonization and Mappings
<a href="https://github.com/nfdi-de/section-metadata-wg-onto/tree/main/sssom">GitHub repository</a>,
run lexical prediction to generate candidate mappings from CHMO, and efficiently
manually curate the results in
<a href="https://github.com/nfdi-de/section-metadata-wg-onto/pull/88">this PR</a>. and
<a href="https://github.com/nfdi-de/section-metadata-wg-onto/pull/89">this PR</a> over the
course of about an hour.</p>

<h2 id="need-for-comparison">Need for Comparison</h2>

<p>The next challenge was to efficiently triage the similarities and differences
between my curations and Ambika’s. Therefore, I implemented a workflow for
comparing the manually curated mappings in two SSSOM documents in
<a href="https://github.com/cthoyt/sssom-pydantic/pull/141">cthoyt/sssom-pydantic#141</a>.
This workflow creates a Markdown file describing similarities and differences.</p>

<p>I chained together the following two CLI commands with <code class="language-plaintext highlighter-rouge">sssom_pydantic</code> to get
the separate mapping files from Ambika’s branch in the NFDI4Chem fork of CHMO,
merge them, then run the comparison against my own curations. Note that these
won’t be reproducible after the branch is merged and deleted, and the actual
results will change as more curation is done.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>sssom_pydantic merge <span class="se">\</span>
<span class="go">        --input https://github.com/NFDI4Chem/rsc-cmo/raw/refs/heads/Add-tsv-files/src/mappings/fix-mappings.sssom.tsv \
        --input https://github.com/NFDI4Chem/rsc-cmo/raw/refs/heads/Add-tsv-files/src/mappings/afo-mappings.sssom.tsv \
        --input https://github.com/NFDI4Chem/rsc-cmo/raw/refs/heads/Add-tsv-files/src/mappings/rex-mappings.sssom.tsv \
        --input https://github.com/NFDI4Chem/rsc-cmo/raw/refs/heads/Add-tsv-files/src/mappings/wikidata-mappings.sssom.tsv \
        --standardize \
        --output ambika.sssom.tsv
</span><span class="gp">$</span><span class="w"> </span>sssom_pydantic compare <span class="se">\</span>
<span class="go">    ambika.sssom.tsv \
    https://github.com/nfdi-de/section-metadata-wg-onto/raw/refs/heads/main/sssom/data/positive.sssom.tsv \
    --standardize \
    --standardize-flip \
    --left-label Ambika \
    --right-label Charlie
</span></code></pre></div></div>

<p>Since the comparison workflow outputs Markdown, its results can easily be
embedded in GitHub issues or my blog, which is itself written in Markdown.</p>

<h2 id="results">Results</h2>

<p>I am happy with the first version of the comparison workflow. Luckily, there
were only a small number of discrepancies which have obvious solutions. There
were also a few interesting discrepancies which were novel to either my or
Ambika’s curations, which can be reviewed by a third curator (sorry Philip, more
work for you).</p>

<h2 id="next-steps">Next Steps</h2>

<p>I think that it can be extended to identify and report on one-to-many,
many-to-one, and many-to-many mappings which arise when jointly examining two
mapping sets. After Philip and others interact with the results, I’m sure we
will be able to extend it with other analyses.</p>

<p>More generally, the implementation of the comparison workflow is part of a
larger suite of workflows that I would like to describe in future posts
including:</p>

<ol>
  <li><a href="https://github.com/cthoyt/sssom-pydantic/pull/136">merging manually curated mappings</a></li>
  <li><a href="https://github.com/cthoyt/sssom-pydantic/pull/128">generating OWL ontology bridges</a></li>
  <li>incorporating SSSOM into ODK builds, which I will support
<a href="https://github.com/gouttegd">Damien Goutte-Gattat</a> to document in the ODK
repository and the <a href="https://oboacademy.github.io/obook">OBOOK</a>.</li>
  <li>unify this analysis with my other idea for doing
<a href="https://github.com/cthoyt/sssom-pydantic/pull/131">automated evaluation of predicted mappings</a>,
which I hope can be used to run future mapping challenges</li>
</ol>

<p>Without further ado, here’s the comparison, copied verbatim from the output of
the previous command:</p>

<h1 id="comparison-between-ambika-and-charlie">Comparison between Ambika and Charlie</h1>

<ol>
  <li><a href="#chmo-to-fix">CHMO to FIX</a></li>
  <li><a href="#chmo-to-rex">CHMO to REX</a></li>
</ol>

<h2 id="chmo-to-fix">CHMO to FIX</h2>

<h3 id="subject-comparison">Subject Comparison</h3>

<ul>
  <li>288 entities appear as subjects only in Ambika</li>
  <li>19 entities appear as subjects only in Charlie only</li>
  <li>138 entities appear as subjects in both</li>
</ul>

<p>The following 6 subjects (4.3%) appearing in both have conflicting objects:</p>

<table>
  <thead>
    <tr>
      <th>subject_id</th>
      <th>subject_label</th>
      <th>Ambika</th>
      <th>both</th>
      <th>Charlie</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CHMO:0000141</td>
      <td>diffraction method</td>
      <td>FIX:0000004 (crystallography)</td>
      <td>FIX:0000217 (diffraction method)</td>
      <td> </td>
    </tr>
    <tr>
      <td>CHMO:0000164</td>
      <td>electron scattering</td>
      <td>FIX:0000666 (electron scattering spectroscopy)</td>
      <td>FIX:0000401 (electron scattering)</td>
      <td> </td>
    </tr>
    <tr>
      <td>CHMO:0000255</td>
      <td>flame atomic emission spectroscopy</td>
      <td>FIX:0000935 (spark method)</td>
      <td>FIX:0000928 (flame atomic emission spectroscopy)</td>
      <td> </td>
    </tr>
    <tr>
      <td>CHMO:0000307</td>
      <td>X-ray emission spectroscopy</td>
      <td>FIX:0000673 (X-ray fluorescence spectroscopy)</td>
      <td>FIX:0000100 (X-ray emission spectroscopy)</td>
      <td> </td>
    </tr>
    <tr>
      <td>CHMO:0000366</td>
      <td>electron energy loss spectroscopy</td>
      <td>FIX:0000664 (electron impact spectroscopy)</td>
      <td>FIX:0000663 (electron energy loss spectroscopy)</td>
      <td> </td>
    </tr>
    <tr>
      <td>CHMO:0000570</td>
      <td>proton transfer reaction ion trap mass spectrometry</td>
      <td>FIX:0000919 (proton transfer reaction ion trap mass spectrometry)</td>
      <td> </td>
      <td>FIX:0000918 (proton transfer reaction mass spectrometry)</td>
    </tr>
  </tbody>
</table>

<h3 id="object-comparison">Object Comparison</h3>

<ul>
  <li>296 entities appear as objects only in Ambika</li>
  <li>19 entities appear as objects only in Charlie</li>
  <li>138 entities appear as objects in both</li>
</ul>

<p>The following 2 objects (1.4%) appearing in both have conflicting subjects:</p>

<table>
  <thead>
    <tr>
      <th>object_id</th>
      <th>object_label</th>
      <th>Ambika</th>
      <th>both</th>
      <th>Charlie</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>FIX:0000629</td>
      <td>pulsed field gel electrophoresis</td>
      <td>CHMO:0002315 (pulsed-field electrophoresis)</td>
      <td> </td>
      <td>CHMO:0002316 (pulsed-field gel electrophoresis)</td>
    </tr>
    <tr>
      <td>FIX:0000816</td>
      <td>square-wave polarography</td>
      <td>CHMO:0000040 (square-wave voltammetry)</td>
      <td>CHMO:0000035 (square-wave polarography)</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<h3 id="subject-object-pair-comparison">Subject-Object Pair Comparison</h3>

<ul>
  <li>301 subject-object pairs only appear in Ambika</li>
  <li>20 subject-object pairs only appear in Charlie</li>
  <li>137 subject-object pairs appear in both</li>
</ul>

<p>The following 1 subject-object pairs (0.7%) appearing in have conflicting
predicates or predicate modifiers:</p>

<table>
  <thead>
    <tr>
      <th>subject_id</th>
      <th>subject_label</th>
      <th>object_id</th>
      <th>object_label</th>
      <th>warning</th>
      <th>Ambika</th>
      <th>Charlie</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CHMO:0000164</td>
      <td>electron scattering</td>
      <td>FIX:0000401</td>
      <td>electron scattering</td>
      <td>different predicate</td>
      <td>skos:narrowMatch</td>
      <td>skos:exactMatch</td>
    </tr>
  </tbody>
</table>

<h2 id="chmo-to-rex">CHMO to REX</h2>

<h3 id="subject-comparison-1">Subject Comparison</h3>

<ul>
  <li>1 entities appear as subjects only in Ambika</li>
  <li>18 entities appear as subjects only in Charlie only</li>
  <li>0 entities appear as subjects in both</li>
</ul>

<h3 id="object-comparison-1">Object Comparison</h3>

<ul>
  <li>1 entities appear as objects only in Ambika</li>
  <li>18 entities appear as objects only in Charlie</li>
  <li>0 entities appear as objects in both</li>
</ul>

<h3 id="subject-object-pair-comparison-1">Subject-Object Pair Comparison</h3>

<ul>
  <li>1 subject-object pairs only appear in Ambika</li>
  <li>18 subject-object pairs only appear in Charlie</li>
  <li>0 subject-object pairs appear in both</li>
</ul>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="SSSOM" /><category term="semantic mappings" /><category term="biocuration" /><category term="curator agreement" /><summary type="html"><![CDATA[I am currently supporting Philip Strömert and Noura Rayya in the efforts to modernize and revitalize the Chemical Methods Ontology (CHMO) to support annotation of instrumentation used to produce experimental data captured in the Chemotion electronic laboratory notebook as part of NFDIChem. This post is about the adoption of Simple Standard for Sharing Ontological Mappings (SSSOM) to support interoperability between CHMO and other resources, and the workflow I developed to compare overlapping manual curations from different researchers.]]></summary></entry><entry><title type="html">A practical approach to translate OWL to SKOS</title><link href="https://cthoyt.com/2026/06/03/owl-to-skos.html" rel="alternate" type="text/html" title="A practical approach to translate OWL to SKOS" /><published>2026-06-03T10:11:00+00:00</published><updated>2026-06-03T10:11:00+00:00</updated><id>https://cthoyt.com/2026/06/03/owl-to-skos</id><content type="html" xml:base="https://cthoyt.com/2026/06/03/owl-to-skos.html"><![CDATA[<p>The data model in <a href="https://github.com/biopragmatics/pyobo">PyOBO</a> closely
resembles the Web Ontology Language (OWL) and macros in the
<a href="https://owlcollab.github.io/oboformat/doc/GO.format.obo-1_4.html">OBO Flat File format</a>.
However, some communities prefer the simplified nature of the SKOS data model
compared to the highly precise (and sometimes burdensome) nature of OWL
semantics. This post describes the workflow I implemented to
<a href="https://github.com/biopragmatics/pyobo/pull/509">downscale OWL to SKOS</a>.</p>

<p>Before diving into this, I searched for previous work in mapping OWL to SKOS and
was surprised to find only a few things (and they’re all old):</p>

<ul>
  <li><a href="https://www.cs.man.ac.uk/~stevensr/papers/iswc2009DC.pdf">Exploring the Relationships between OWL and SKOS</a>
by Nor Azlinayati Abdul Manaf (2009)</li>
  <li><a href="https://www.w3.org/2006/07/SWD/SKOS/skos-and-owl/master.html">Using OWL and SKOS</a>
by Sean Bechhofer and Alstair Miles (2008)</li>
  <li><a href="https://www.heppnetz.de/projects/skos2owl/">SKOS2OWL</a> by Hepp <em>et al.</em> (2007)</li>
</ul>

<h2 id="predicate-mappings">Predicate Mappings</h2>

<p>However, I didn’t find these resources very helpful, so I just jumped right into
an implementation. First, I mapped several predicates used in OWL to predicates
used in SKOS:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">rdfs:label</code> maps to <code class="language-plaintext highlighter-rouge">skos:prefLabel</code></li>
  <li>synonym predicates from OBO-flavored OWL (<code class="language-plaintext highlighter-rouge">oboInOwl:hasExactSynonym</code>,
<code class="language-plaintext highlighter-rouge">oboInOwl:hasNarrowSynonym</code>, <code class="language-plaintext highlighter-rouge">oboInOwl:hasBroadSynonym</code>, and
<code class="language-plaintext highlighter-rouge">oboInOwl:hasRelatedSynonym</code>) are squashed to <code class="language-plaintext highlighter-rouge">skos:altLabel</code>, and synonym
type information is thrown away</li>
  <li><code class="language-plaintext highlighter-rouge">dcterms:description</code> maps to <code class="language-plaintext highlighter-rouge">skos:definition</code></li>
  <li><code class="language-plaintext highlighter-rouge">rdf:subClassOf</code> and <code class="language-plaintext highlighter-rouge">rdf:type</code> are squashed to <code class="language-plaintext highlighter-rouge">skos:broadMatch</code></li>
  <li>similarly, individuals (<code class="language-plaintext highlighter-rouge">owl:NamedIndividual</code>) and classes (<code class="language-plaintext highlighter-rouge">owl:Class</code>) are
both squashed to <code class="language-plaintext highlighter-rouge">skos:Concept</code></li>
  <li>predicates and relationships are omitted</li>
  <li>root terms annotated on the ontology with <code class="language-plaintext highlighter-rouge">IAO:0000700</code> (ontology has root
term) get annotated on the SKOS concept scheme with <code class="language-plaintext highlighter-rouge">skos:hasTopConcept</code></li>
</ul>

<h2 id="hierarchical-reasoning">Hierarchical Reasoning</h2>

<p>I introduced
<a href="https://semantic.farm/OMO:0003014"><code class="language-plaintext highlighter-rouge">OMO:0003014</code> (has ontology hierarchical property)</a>
in
<a href="https://github.com/information-artifact-ontology/ontology-metadata/pull/193">information-artifact-ontology/ontology-metadata#193</a>
to enable ontologies to explicitly annotate which predicates should be used
during hierarchical browsing (in addition to <code class="language-plaintext highlighter-rouge">rdf:subClassOf</code>). For example, the
<a href="https://semantic.farm/uberon">Uber Anatomy Ontology (UBERON)</a> contains a
partonomy of anatomical features, and should therefore be browsable based on the
<a href="https://semantic.farm/BFO:0000050"><code class="language-plaintext highlighter-rouge">BFO:0000050</code> (part of)</a> relationship. This
annotation was originally inspired by the custom configuration for adding new
ontologies to the EBI’s
<a href="https://www.ebi.ac.uk/ols4/">Ontology Lookup Service (OLS)</a> - now, the
configuration for the OLS can be upstreamed into the ontologies themselves and
be made more reusable.</p>

<p>Accordingly, I used hierarchical properties annotated on to the ontology with
<code class="language-plaintext highlighter-rouge">OMO:0003014</code> to translate object property constraints into broad matches.
Inside PyOBO, I updated the source for the
<a href="https://ror.org/">Research Organization Registry (ROR)</a> to explicitly annotate
<a href="https://semantic.farm/org:subOrganizationOf"><code class="language-plaintext highlighter-rouge">org:subOrganizationOf</code></a> as a
hierarchical property. Below, the entry for the French
<a href="https://ror.org/000063q30">Techniques of Informatics and Microelectronics for Integrated Systems Architecture</a>
shows the translation into broad matches for both the subclass relation to the
<em>ad hoc</em> term for a facility and the sub-organization relations to its four
parent organizations:</p>

<pre><code class="language-ttl">ror:000063q30 a skos:Concept ;
    skos:altLabel "TIMA",
        "TIMA Laboratory",
        "Techniques de l'Informatique et de la Microélectronique pour l'Architecture des Systèmes Intégrés"@fr ;
    skos:broadMatch &lt;http://purl.obolibrary.org/obo/ror#facility&gt;,
        ror:02feahw73,
        ror:02rx3b187,
        ror:04z22qz54,
        ror:05sbt2524 ;
    skos:prefLabel "Techniques of Informatics and Microelectronics for Integrated Systems Architecture" ;
    skos:scopeNote "a facility in Grenoble established in 2003" .
</code></pre>

<h2 id="inverse-hierarchical-reasoning">Inverse Hierarchical Reasoning</h2>

<p>I extended this notion further to leverage inverse predicate information for the
hierarchical property annotations. If the inverse of a hierarchical property is
used in an object property constraint (or annotation property), then it is
translated into a narrow match.</p>

<p>For example, in FamPlex, complexes are annotated with the
<a href="https://semantic.farm/RO:0002351"><code class="language-plaintext highlighter-rouge">RO:0002351</code> (has member)</a> relationship to
genes. This algorithm looks up the inverse relationship
<a href="https://semantic.farm/RO:0002350"> <code class="language-plaintext highlighter-rouge">RO:0002350</code> (is member of)</a> and annotates
that as a complex has narrow match to gene triple, and simultaneously, gene has
broad match to complex triple.</p>

<pre><code class="language-ttl">fplx:Sarcoglycan_complex a skos:Concept ;
    skos:broadMatch fplx:DGC ;
    skos:inScheme &lt;https://w3id.org/biopragmatics/resources/fplx/fplx.ttl&gt; ;
    skos:narrowMatch hgnc:10805,
        hgnc:10806,
        hgnc:10807,
        hgnc:10808,
        hgnc:14075 ;
    skos:prefLabel "Sarcoglycan_complex" ;
    skos:scopeNote "A family of transmembrane dystrophin-associated proteins that play a role in the membrane association of the DYSTROPHIN-ASSOCIATED PROTEIN COMPLEX." .
</code></pre>

<h2 id="more-or-less-semantics">More or Less, Semantics</h2>

<p>We have the goal in NFDI4Chem to map from ontology terms in the Chemical Methods
Ontology (CHMO) to the <a href="https://goldbook.iupac.org">IUPAC GoldBook</a>, a
compendium of chemical terminology which also covers instrumentation and
experimental techniques.</p>

<p>While GoldBook’s data is now available in both JSON and XML, neither ascribe
semantics to their fields, i.e., the XML doesn’t use namespaces and the JSON
doesn’t have a <em>linked data</em> component. This meant that as I implemented a PyOBO
source for GoldBook in
<a href="https://github.com/biopragmatics/pyobo/pull/436">biopragmatics/pyobo#436</a>, I
had to interpret what each field means, and ascribe semantics to them by
assigning existing, well-known predicates to each.</p>

<p>However, based on
<a href="https://github.com/NFDI4Chem/Ontologies4Chem2026/discussions/7">discussions</a> in
preparation for the 6<sup>th</sup> Ontologies4Chem workshop later this year, it
appears IUPAC does not want to make ontological commitments for its entities,
and would instead prefer to produce a SKOS vocabulary.</p>

<p>My work on ontologizing GoldBook was done without discussion with IUPAC, similar
to most PyOBO sources. Personally, I think it’s much more valuable to ascribe
precise semantics. I am less dogmatic about ontological commitments and have
usually focused on what makes a data resource useful for me. In this case, it
was being able to access GoldBook through an ontology-like interface such that
it could be loaded in SSSOM Curator to
<a href="https://github.com/biopragmatics/biomappings/pull/240">lexically predict then manually curate CHMO-GoldBook mappings</a>.</p>

<p>However, now with the SKOS exporter described earlier in this post, I can have
it both ways: first starting with a more precise OWL artifact, then downscaling
to a simpler SKOS artifact with entries that look like:</p>

<pre><code class="language-ttl">goldbook:08003 a skos:Concept ;
    skos:inScheme &lt;https://w3id.org/biopragmatics/resources/goldbook/goldbook.ttl&gt; ;
    skos:prefLabel "analytical chemistry" ;
    skos:scopeNote "Scientific discipline that develops and applies strategies, instruments, and procedures to obtain information on the composition and nature of matter in space and time." .
</code></pre>

<hr />

<p>There are still a few parts of SKOS that I’m not familiar with, so I expect that
this translation will evolve over time. For example, because SKOS is so tightly
tied to RDF as a serialization, it has better support for language tags. OWL can
be serialized in RDF, but the OBO Flat File Format is inherently limited in its
ability to express language. I’m interested to overcome these limits in the
PyOBO implementation.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="OWL" /><category term="SKOS" /><category term="PyOBO" /><summary type="html"><![CDATA[The data model in PyOBO closely resembles the Web Ontology Language (OWL) and macros in the OBO Flat File format. However, some communities prefer the simplified nature of the SKOS data model compared to the highly precise (and sometimes burdensome) nature of OWL semantics. This post describes the workflow I implemented to downscale OWL to SKOS.]]></summary></entry><entry><title type="html">Transparent provenance for derived semantic mappings in SSSOM</title><link href="https://cthoyt.com/2026/05/29/sssom-derived-from.html" rel="alternate" type="text/html" title="Transparent provenance for derived semantic mappings in SSSOM" /><published>2026-05-29T10:11:00+00:00</published><updated>2026-05-29T10:11:00+00:00</updated><id>https://cthoyt.com/2026/05/29/sssom-derived-from</id><content type="html" xml:base="https://cthoyt.com/2026/05/29/sssom-derived-from.html"><![CDATA[<p>The
<a href="https://mapping-commons.github.io/sssom">Simple Standard for Sharing Ontological Mappings (SSSOM)</a>
requires semantic mappings to annotate a <em>justification</em> from the
<a href="https://semantic.farm/semapv">Semantic Mapping Vocabulary (SEMAPV)</a> such as
<a href="https://semantic.farm/semapv:ManualMappingCuration"><code class="language-plaintext highlighter-rouge">semapv:ManualMappingCuration</code></a>,
<a href="https://semantic.farm/semapv:MappingInversion"><code class="language-plaintext highlighter-rouge">semapv:MappingInversion</code></a>, and
<a href="https://semantic.farm/semapv:MappingChaining"><code class="language-plaintext highlighter-rouge">semapv:MappingChaining</code></a>.
However, SSSOM did not have a mechanism for tracking which mappings were used
during inference workflows like inversion or chaining. This post is about my
recent
<a href="https://github.com/mapping-commons/sssom/issues/537">SSSOM enhancement proposal</a>
to add a new field <code class="language-plaintext highlighter-rouge">derived_from</code> to support this.</p>

<h2 id="background">Background</h2>

<p>Before we get started, it’s worth defining a few terms about mappings to make
sure that we stay precise.</p>

<h3 id="mapping-triple">Mapping Triple</h3>

<p>A <em>mapping triple</em> is a subject, predicate, and object such as <code class="language-plaintext highlighter-rouge">mesh:C000089</code>,
<code class="language-plaintext highlighter-rouge">skos:exactMatch</code>, <code class="language-plaintext highlighter-rouge">CHEBI:28646</code>. By semantic web conventions based on the open
world assumption (OWA), triples are assumed to be true unless stated otherwise.</p>

<pre><code class="language-mermaid">flowchart LR
    mesh:C000089[ammeline
mesh:C000089]
CHEBI:28646[ammeline
CHEBI:28646]

mesh:C000089--&gt;|skos:exactMatch|CHEBI:28646
</code></pre>

<h3 id="mapping-quadruple">Mapping Quadruple</h3>

<p>A <em>mapping quadruple</em> is a subject, predicate, object, and truthiness (a
boolean). SSSOM represents the truthiness using the <code class="language-plaintext highlighter-rouge">predicate_modifier</code> field,
where an empty value means that the mapping is true, and a value of <code class="language-plaintext highlighter-rouge">Not</code> means
that it is false.</p>

<p>For example, (<code class="language-plaintext highlighter-rouge">mesh:C000089</code>, <code class="language-plaintext highlighter-rouge">skos:exactMatch</code>, <code class="language-plaintext highlighter-rouge">CHEBI:28646</code>, <code class="language-plaintext highlighter-rouge">True</code>) makes
the example <em>mapping triple</em> from above into a mapping quadruple that explicitly
states its truthiness.</p>

<pre><code class="language-mermaid">flowchart LR
    mesh:C000089[ammeline
mesh:C000089]
CHEBI:28646[ammeline
CHEBI:28646]

mesh:C000089--&gt;|"skos:exactMatch\n(true)"|CHEBI:28646
</code></pre>

<p>Conversely, (<code class="language-plaintext highlighter-rouge">CHEBI:10057</code>, <code class="language-plaintext highlighter-rouge">skos:exactMatch</code>, <code class="language-plaintext highlighter-rouge">mesh:C002563</code>, <code class="language-plaintext highlighter-rouge">False</code>) is false
because <code class="language-plaintext highlighter-rouge">CHEBI:10057</code> refers to 9H-xanthene, a small molecule, and
<code class="language-plaintext highlighter-rouge">mesh:C002563</code> refers to xanthan gum, a polysaccharide.</p>

<pre><code class="language-mermaid">flowchart LR
    mesh:C002563[xanthan gum
mesh:C000089]
CHEBI:10057[9H-xanthene
CHEBI:10057]

mesh:C002563--&gt;|"not skos:exactMatch"|CHEBI:10057
</code></pre>

<p>By convention, mapping triples are implicitly considered to refer to the
corresponding <em>true</em> mapping quadruple.</p>

<h3 id="mapping-record">Mapping Record</h3>

<p>A <em>mapping record</em> is a subject, predicate, object, truthiness, and other
metadata about the mapping that appears in the SSSOM
<a href="https://mapping-commons.github.io/sssom/Mapping/">Mapping</a> data model. A
mapping record best be visualized by reifying the triple (i.e., make into a
node) to show how additional metadata is connected.</p>

<pre><code class="language-mermaid">flowchart LR
    mesh:C000089[ammeline
mesh:C000089]
CHEBI:28646[ammeline
CHEBI:28646]
1181E81AD58C1B94("mesh:C000089
skos:exactMatch
CHEBI:28646
manual curation")
style 1181E81AD58C1B94 fill:#bbf
orcid:0000-0003-4423-4370[Charles Tapley Hoyt
orcid:0000-0003-4423-4370]
style orcid:0000-0003-4423-4370 fill: #bef
wikidata:Q111239110[Biomappings
wikidata:Q111239110]
style wikidata:Q111239110 fill: #feb
1181E81AD58C1B94 --&gt;|has author|orcid:0000-0003-4423-4370
1181E81AD58C1B94--&gt;|source|wikidata:Q111239110
mesh:C000089 --&gt;|subject of|1181E81AD58C1B94
CHEBI:28646 --&gt;|object of|1181E81AD58C1B94
</code></pre>

<p>Note that I’m playing a bit fast and loose with the semantics of the predicates
relating the mapping record to other components. This is explicitly specified as
part of SSSOM, but I don’t think super instructive here, so I’ve used more
human-readable labels.</p>

<h2 id="motivation">Motivation</h2>

<p>I’ve been thinking about the large-scale aggregation and inference over semantic
mappings for several years now. My initial work resulted in the implementation
of the
<a href="https://github.com/biopragmatics/semra">Semantic Mapping Reasoner and Assembler (SeMRA)</a>
and the publication of its companion article
<a href="https://doi.org/10.1093/bioinformatics/btaf542">Assembly and reasoning over semantic mappings at scale for biomedical data integration</a>.</p>

<p>Initially, SeMRA implemented a custom data model that was similar to SSSOM, but
had a more explicit provenance model for which mapping records (e.g., from
SSSOM) were used to infer/derive new mapping quadruples. As SeMRA and its
applications have matured, I have been able to backport many of its good ideas
to SSSOM. This post is specifically about how I’ve proposed a simple, optional,
explicit provenance model that allows mapping records in SSSOM derived through
inference to reference the set of mapping quadruples that were used during
inference, in a new <code class="language-plaintext highlighter-rouge">derived_from</code> slot.</p>

<h3 id="reference-the-triple-quadruple-or-record">Reference the Triple, Quadruple, or Record?</h3>

<p>This slot is deliberately under-specified, but there’s a key nuance that the
values in this field should point to identifiers for mapping quadruples, not
mapping triples nor mapping records for the following reasons:</p>

<ol>
  <li>Mapping triples are insufficient: without the judgment of whether a mapping
is true or false, then an algorithm could accidentally conclude from
<code class="language-plaintext highlighter-rouge">A skos:exactMatch B</code> and <code class="language-plaintext highlighter-rouge">B (not) skos:exactMatch C</code> that
<code class="language-plaintext highlighter-rouge">A skos:exactMatch C</code>. This is why mapping triples are insufficient</li>
  <li>Inference happens on the mapping quadruple level, so it matches nicely</li>
  <li>Full mapping records are inflexible: the SSSOM data should be flexible so if
additional evidence (i.e., records) for a given mapping quadruple are found,
then the confidence in the inferred/derived mapping (e.g., chained or
inverted) can be adjusted accordingly. This is possible because most chaining
and inversion algorithms logically operate on mapping quadruples, and not on
records.</li>
</ol>

<p>This is why in the following diagrams, there are mapping quads represented in
pink boxes and mapping records represented by purple boxes.</p>

<p>Note: the local unique identifiers used for mappings in this example are related
to the proposal in https://github.com/ts4nfdi/mapping-sameness-identifier (which
currently is under review). For now, the SSSOM specification isn’t prescribing
how to assign identifiers to mapping quadruples.</p>

<h2 id="contribution">Contribution</h2>

<p>See the original issue
<a href="https://github.com/mapping-commons/sssom/issues/537">mapping-commons/sssom#537</a>
and pull request
<a href="https://github.com/mapping-commons/sssom/pull/548">mapping-commons/sssom#548</a>.
This post is an extension of the documentation I wrote in that PR
(<a href="https://mapping-commons.github.io/sssom/dev/inference">https://mapping-commons.github.io/sssom/dev/inference</a>).</p>

<p>I implemented the <code class="language-plaintext highlighter-rouge">derived_from</code> field in
<a href="https://github.com/cthoyt/sssom-pydantic">SSSOM Pydantic</a> in
<a href="https://github.com/cthoyt/sssom-pydantic/pull/108">cthoyt/sssom-pydantic#108</a>.
Subsequently, while preparing this post, I implemented a mechanism for
generating the pretty <a href="https://mermaid.js.org/">Mermaid</a> diagrams I’ve used
throughout this post in
<a href="https://github.com/cthoyt/sssom-pydantic/pull/129">cthoyt/sssom-pydantic#129</a>.</p>

<p>Here’s how this looks in Python:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">curies</span> <span class="kn">import</span> <span class="n">Converter</span><span class="p">,</span> <span class="n">NamableReference</span><span class="p">,</span> <span class="n">NamedReference</span>
<span class="kn">from</span> <span class="nn">curies.vocabulary</span> <span class="kn">import</span> <span class="n">charlie</span><span class="p">,</span> <span class="n">manual_mapping_curation</span><span class="p">,</span> <span class="n">mapping_chaining</span><span class="p">,</span> <span class="n">exact_match</span>

<span class="kn">from</span> <span class="nn">sssom_pydantic</span> <span class="kn">import</span> <span class="n">SemanticMapping</span><span class="p">,</span> <span class="n">hash_triple_to_reference</span>

<span class="n">CONVERTER</span> <span class="o">=</span> <span class="n">Converter</span><span class="p">.</span><span class="n">from_prefix_map</span><span class="p">({</span>
    <span class="s">"BTO"</span><span class="p">:</span> <span class="s">"http://purl.obolibrary.org/obo/CHEBI_"</span><span class="p">,</span>
    <span class="s">"CL"</span><span class="p">:</span> <span class="s">"http://purl.obolibrary.org/obo/CL_"</span><span class="p">,</span>
    <span class="s">"mesh"</span><span class="p">:</span> <span class="s">"http://id.nlm.nih.gov/mesh/"</span><span class="p">,</span>
    <span class="s">"wikidata"</span><span class="p">:</span> <span class="s">"http://www.wikidata.org/entity/"</span><span class="p">,</span>
    <span class="s">"orcid"</span><span class="p">:</span> <span class="s">"https://orcid.org/"</span><span class="p">,</span>
    <span class="s">"semapv"</span><span class="p">:</span> <span class="s">"https://w3id.org/semapv/vocab/"</span><span class="p">,</span>
    <span class="s">"skos"</span><span class="p">:</span> <span class="s">"http://www.w3.org/2004/02/skos/core#"</span><span class="p">,</span>

<span class="p">})</span>

<span class="n">E1</span> <span class="o">=</span> <span class="n">NamedReference</span><span class="p">.</span><span class="n">from_curie</span><span class="p">(</span><span class="s">"BTO:0006078"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"pluripotent stem cell"</span><span class="p">)</span>
<span class="n">E2</span> <span class="o">=</span> <span class="n">NamedReference</span><span class="p">.</span><span class="n">from_curie</span><span class="p">(</span><span class="s">"CL:0002248"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"pluripotent stem cell"</span><span class="p">)</span>
<span class="n">E3</span> <span class="o">=</span> <span class="n">NamedReference</span><span class="p">.</span><span class="n">from_curie</span><span class="p">(</span><span class="s">"mesh:D039904"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"Pluripotent Stem Cells"</span><span class="p">)</span>
<span class="n">SOURCE</span> <span class="o">=</span> <span class="n">NamableReference</span><span class="p">.</span><span class="n">from_curie</span><span class="p">(</span><span class="s">"wikidata:Q111239110"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"Biomappings"</span><span class="p">)</span>

<span class="n">m1</span> <span class="o">=</span> <span class="n">SemanticMapping</span><span class="p">(</span>
    <span class="n">subject</span><span class="o">=</span><span class="n">E1</span><span class="p">,</span>
    <span class="n">predicate</span><span class="o">=</span><span class="n">exact_match</span><span class="p">,</span>
    <span class="nb">object</span><span class="o">=</span><span class="n">E2</span><span class="p">,</span>
    <span class="n">justification</span><span class="o">=</span><span class="n">manual_mapping_curation</span><span class="p">,</span>
    <span class="n">authors</span><span class="o">=</span><span class="p">[</span><span class="n">charlie</span><span class="p">],</span>
    <span class="n">source</span><span class="o">=</span><span class="n">SOURCE</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">m2</span> <span class="o">=</span> <span class="n">SemanticMapping</span><span class="p">(</span>
    <span class="n">subject</span><span class="o">=</span><span class="n">E2</span><span class="p">,</span>
    <span class="n">predicate</span><span class="o">=</span><span class="n">exact_match</span><span class="p">,</span>
    <span class="nb">object</span><span class="o">=</span><span class="n">E3</span><span class="p">,</span>
    <span class="n">justification</span><span class="o">=</span><span class="n">manual_mapping_curation</span><span class="p">,</span>
    <span class="n">authors</span><span class="o">=</span><span class="p">[</span><span class="n">charlie</span><span class="p">],</span>
    <span class="n">source</span><span class="o">=</span><span class="n">SOURCE</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">m3</span> <span class="o">=</span> <span class="n">SemanticMapping</span><span class="p">(</span>
    <span class="n">subject</span><span class="o">=</span><span class="n">E1</span><span class="p">,</span>
    <span class="n">predicate</span><span class="o">=</span><span class="n">exact_match</span><span class="p">,</span>
    <span class="nb">object</span><span class="o">=</span><span class="n">E3</span><span class="p">,</span>
    <span class="n">justification</span><span class="o">=</span><span class="n">mapping_chaining</span><span class="p">,</span>
    <span class="c1"># This is the new part!
</span>    <span class="n">derived_from</span><span class="o">=</span><span class="p">[</span>
        <span class="n">hash_triple_to_reference</span><span class="p">(</span><span class="n">m1</span><span class="p">,</span> <span class="n">CONVERTER</span><span class="p">),</span>
        <span class="n">hash_triple_to_reference</span><span class="p">(</span><span class="n">m2</span><span class="p">,</span> <span class="n">CONVERTER</span><span class="p">),</span>
    <span class="p">],</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Damien Goutte-Gattat also implemented the <code class="language-plaintext highlighter-rouge">derived_from</code> in
<a href="https://github.com/gouttegd/sssom-java">SSSOM Java</a> in
<a href="https://github.com/gouttegd/sssom-java/pull/19">gouttegd/sssom-java#19</a>.</p>

<h2 id="examples">Examples</h2>

<p>To finish off this post, I’ve included five real-world examples combining
manually curated mappings from
<a href="https://github.com/biopragmatics/biomappings">Biomappings</a> and the ChEBI
ontology that show off three types of inference: inversion, chaining, and
background knowledge-based mapping.</p>

<h3 id="inversion">Inversion</h3>

<p>A mapping inversion workflow inverts the mapping predicate then swaps subject
and object components. For example, the mapping predicate <code class="language-plaintext highlighter-rouge">skos:narrowMatch</code>
and<code class="language-plaintext highlighter-rouge">skos:broadMatch</code> are inverses and <code class="language-plaintext highlighter-rouge">skos:exactMatch</code> is its own inverse. More
information about mapping predicates can be found in the
<a href="https://mapping-commons.github.io/sssom/mapping-predicates/">SSSOM documentation</a>.</p>

<p>The introduction of the <code class="language-plaintext highlighter-rouge">derived_from</code> field allows an inverted mapping record
to refer back to the mapping it was derived from.</p>

<p>In the example below, a manually curated mapping between MeSH and ChEBI’s terms
for ammeline is inverted.</p>

<pre><code class="language-mermaid">flowchart LR
    mesh:C000089[ammeline
mesh:C000089]
CHEBI:28646[ammeline
CHEBI:28646]
1181E81AD58C1B94("mesh:C000089
skos:exactMatch
CHEBI:28646
manual curation")
style 1181E81AD58C1B94 fill:#bbf
orcid:0000-0003-4423-4370[Charles Tapley Hoyt
orcid:0000-0003-4423-4370]
style orcid:0000-0003-4423-4370 fill: #bef
wikidata:Q111239110[Biomappings
wikidata:Q111239110]
style wikidata:Q111239110 fill: #feb
36a1f9244ea7641a90987c82f33c25c0c13712ee8f48207b2a0825f8a4e4e26a[["mesh:C000089
skos:exactMatch
CHEBI:28646"]]
style 36a1f9244ea7641a90987c82f33c25c0c13712ee8f48207b2a0825f8a4e4e26a fill: #f9f
54D1553627DC5514("CHEBI:28646
skos:exactMatch
mesh:C000089
inversion")
style 54D1553627DC5514 fill: #bbf
faeefc6d1dd08238a9732de5a3c9dcf99388e62fa8b1caaf9ba28c7eaf6d483a[["CHEBI:28646
skos:exactMatch
mesh:C000089"]]
style faeefc6d1dd08238a9732de5a3c9dcf99388e62fa8b1caaf9ba28c7eaf6d483a fill: #f9f
1181E81AD58C1B94 --&gt;|has author|orcid:0000-0003-4423-4370
1181E81AD58C1B94--&gt;|source|wikidata:Q111239110
mesh:C000089 --&gt;|subject of|1181E81AD58C1B94
CHEBI:28646 --&gt;|object of|1181E81AD58C1B94
36a1f9244ea7641a90987c82f33c25c0c13712ee8f48207b2a0825f8a4e4e26a --&gt;|has evidence|1181E81AD58C1B94
CHEBI:28646 --&gt;|subject of|54D1553627DC5514
mesh:C000089 --&gt;|object of|54D1553627DC5514
faeefc6d1dd08238a9732de5a3c9dcf99388e62fa8b1caaf9ba28c7eaf6d483a --&gt;|has evidence|54D1553627DC5514
54D1553627DC5514 --&gt;|derived from|36a1f9244ea7641a90987c82f33c25c0c13712ee8f48207b2a0825f8a4e4e26a
</code></pre>

<details>
<summary>Source SSSOM TSV</summary>

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th>subject_id</th>
      <th>subject_label</th>
      <th>predicate_id</th>
      <th>object_id</th>
      <th>object_label</th>
      <th>mapping_justification</th>
      <th>author_id</th>
      <th>mapping_source</th>
      <th>derived_from</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>mesh:C000089</td>
      <td>ammeline</td>
      <td>skos:exactMatch</td>
      <td>chebi:28646</td>
      <td>ammeline</td>
      <td>semapv:ManualMappingCuration</td>
      <td>orcid:0000-0003-4423-4370</td>
      <td>wikidata:Q111239110</td>
      <td></td>
    </tr>
    <tr>
      <td>chebi:28646</td>
      <td>ammeline</td>
      <td>skos:exactMatch</td>
      <td>mesh:C000089</td>
      <td>ammeline</td>
      <td>semapv:MappingInversion</td>
      <td></td>
      <td></td>
      <td>mapping:36a1f9244ea7641a90987c82f33c25c0c13712ee8f48207b2a0825f8a4e4e26a</td>
    </tr>
  </tbody>
</table>
</details>

<h2 id="chaining">Chaining</h2>

<p>A mapping chaining workflow applies
<a href="https://mapping-commons.github.io/sssom/chaining-rules/">SSSOM chaining rules</a>
to combine one or more mappings who share subjects/objects. The resulting
mappings should be tagged with <code class="language-plaintext highlighter-rouge">semapv:MappingChaining</code> as a justification.</p>

<p>Depending on the implementation, directionality is important, so inferring
inverted mappings before chaining is important.</p>

<pre><code class="language-mermaid">flowchart LR
    BTO:0006078[pluripotent stem cell
BTO:0006078]
CL:0002248[pluripotent stem cell
CL:0002248]
EEB83E3B60649C0E("BTO:0006078
skos:exactMatch
CL:0002248
manual curation")
style EEB83E3B60649C0E fill: #bbf
orcid:0000-0003-4423-4370[Charles Tapley Hoyt
orcid:0000-0003-4423-4370]
style orcid:0000-0003-4423-4370 fill: #bef
wikidata:Q111239110[Biomappings
wikidata:Q111239110]
style wikidata:Q111239110 fill:#feb
8a12a396b85642cccfc799fb24320c51a4aabf3294780cb31116d45f773a2572[["BTO:0006078
skos:exactMatch
CL:0002248"]]
style 8a12a396b85642cccfc799fb24320c51a4aabf3294780cb31116d45f773a2572 fill: #f9f
mesh:D039904[pluripotent stem cells
mesh:D039904]
CD2204936C2DDC93("CL:0002248
skos:exactMatch
mesh:D039904
manual curation")
style CD2204936C2DDC93 fill: #bbf
988ce14e26fdbf24aeb27b4d8b5ad4bcc25b5cdb46be4e674bfa88a2abe12264[["CL:0002248
skos:exactMatch
mesh:D039904"]]
style 988ce14e26fdbf24aeb27b4d8b5ad4bcc25b5cdb46be4e674bfa88a2abe12264 fill: #f9f
AC5F57BF466F5641("BTO:0006078
skos:exactMatch
mesh:D039904
chaining")
style AC5F57BF466F5641 fill: #bbf
0d2804dff03667d435d38f61e97cd8435382ad45104c6be460f9aa318e0a4622[["BTO:0006078
skos:exactMatch
mesh:D039904"]]
style 0d2804dff03667d435d38f61e97cd8435382ad45104c6be460f9aa318e0a4622 fill: #f9f
EEB83E3B60649C0E --&gt;|has author|orcid:0000-0003-4423-4370
EEB83E3B60649C0E--&gt;|source|wikidata:Q111239110
BTO:0006078 --&gt;|subject of|EEB83E3B60649C0E
CL:0002248 --&gt;|object of|EEB83E3B60649C0E
8a12a396b85642cccfc799fb24320c51a4aabf3294780cb31116d45f773a2572 --&gt;|has evidence|EEB83E3B60649C0E
CD2204936C2DDC93 --&gt;|has author|orcid:0000-0003-4423-4370
CD2204936C2DDC93 --&gt;|source|wikidata:Q111239110
CL:0002248 --&gt;|subject of|CD2204936C2DDC93
mesh:D039904 --&gt;|object of|CD2204936C2DDC93
988ce14e26fdbf24aeb27b4d8b5ad4bcc25b5cdb46be4e674bfa88a2abe12264 --&gt;|has evidence|CD2204936C2DDC93
BTO:0006078 --&gt;|subject of|AC5F57BF466F5641
mesh:D039904 --&gt;|object of|AC5F57BF466F5641
0d2804dff03667d435d38f61e97cd8435382ad45104c6be460f9aa318e0a4622--&gt;|has evidence|AC5F57BF466F5641
AC5F57BF466F5641 --&gt;|derived from|8a12a396b85642cccfc799fb24320c51a4aabf3294780cb31116d45f773a2572
AC5F57BF466F5641 --&gt;|derived from|988ce14e26fdbf24aeb27b4d8b5ad4bcc25b5cdb46be4e674bfa88a2abe12264
</code></pre>

<details>
<summary>Source SSSOM TSV</summary>

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th>subject_id</th>
      <th>subject_label</th>
      <th>predicate_id</th>
      <th>object_id</th>
      <th>object_label</th>
      <th>mapping_justification</th>
      <th>author_id</th>
      <th>mapping_source</th>
      <th>derived_from</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>BTO:0006078</td>
      <td>pluripotent stem cell</td>
      <td>skos:exactMatch</td>
      <td>CL:0002248</td>
      <td>pluripotent stem cell</td>
      <td>semapv:ManualMappingCuration</td>
      <td>orcid:0000-0003-4423-4370</td>
      <td>wikidata:Q111239110</td>
      <td></td>
    </tr>
    <tr>
      <td>CL:0002248</td>
      <td>pluripotent stem cell</td>
      <td>skos:exactMatch</td>
      <td>mesh:D039904</td>
      <td>Pluripotent Stem Cells</td>
      <td>semapv:ManualMappingCuration</td>
      <td>orcid:0000-0003-4423-4370</td>
      <td>wikidata:Q111239110</td>
      <td></td>
    </tr>
    <tr>
      <td>BTO:0006078</td>
      <td>pluripotent stem cell</td>
      <td>skos:exactMatch</td>
      <td>mesh:D039904</td>
      <td>Pluripotent Stem Cells</td>
      <td>semapv:MappingChaining</td>
      <td></td>
      <td></td>
      <td>mapping:8a12a396b85642cccfc799fb24320c51a4aabf3294780cb31116d45f773a2572|mapping:988ce14e26fdbf24aeb27b4d8b5ad4bcc25b5cdb46be4e674bfa88a2abe12264</td>
    </tr>
  </tbody>
</table>
</details>

<h2 id="chaining-with-negatives">Chaining with Negatives</h2>

<p>When I wrote the initial
<a href="https://mapping-commons.github.io/sssom/chaining-rules/">SSSOM chaining rules</a>,
I did not include any examples on how negative modifiers interact with the
rules. This example is a prospective look on how negative mappings and positive
mappings could interact. I will be making some improvements to the SSSOM docs
with additional concrete rules soon.</p>

<pre><code class="language-mermaid">flowchart LR
    CHEBI:10057[9H-xanthene
CHEBI:10057]
mesh:C002563[xanthan gum
mesh:C002563]
462D9D4128330C63("CHEBI:10057
not skos:exactMatch
mesh:C002563
manual curation")
style 462D9D4128330C63 fill: #bbf
orcid:0000-0003-4423-4370[Charles Tapley Hoyt
orcid:0000-0003-4423-4370]
style orcid:0000-0003-4423-4370 fill: #bef
wikidata:Q111239110[Biomappings
wikidata:Q111239110]
style wikidata:Q111239110 fill:#feb
58f24ccfaf71431276da873c9e7b77ea61a2425e4e8b283b943542290deb292bN[["CHEBI:10057
not skos:exactMatch
mesh:C002563"]]
style 58f24ccfaf71431276da873c9e7b77ea61a2425e4e8b283b943542290deb292bN fill: #f9f
cas:92-83-1[Xanthene
cas:92-83-1]
190EA9377428B4CB("cas:92-83-1
skos:exactMatch
CHEBI:10057
manual curation")
style 190EA9377428B4CB fill: #bbf
bb1162fb2afb1c519c0aa8be98c352061720af220e2d052c571a1fecabff9800[["cas:92-83-1
skos:exactMatch
CHEBI:10057"]]
style bb1162fb2afb1c519c0aa8be98c352061720af220e2d052c571a1fecabff9800 fill: #f9f
25AC613A93F7EF14("cas:92-83-1
not skos:exactMatch
mesh:C002563
chaining")
style 25AC613A93F7EF14 fill: #bbf
84238c3cc875f0939ee4a8cf76bf33f216784434756833e3ba8d1b1183c372f5N[["cas:92-83-1
not skos:exactMatch
mesh:C002563"]]
style 84238c3cc875f0939ee4a8cf76bf33f216784434756833e3ba8d1b1183c372f5N fill: #f9f
462D9D4128330C63 --&gt;|has author|orcid:0000-0003-4423-4370
462D9D4128330C63 --&gt;|source|wikidata:Q111239110
CHEBI:10057 --&gt;|subject of|462D9D4128330C63
mesh:C002563 --&gt;|object of|462D9D4128330C63
58f24ccfaf71431276da873c9e7b77ea61a2425e4e8b283b943542290deb292bN --&gt;|has evidence|462D9D4128330C63
190EA9377428B4CB --&gt;|has author|orcid:0000-0003-4423-4370
190EA9377428B4CB --&gt;|source|wikidata:Q111239110
cas:92-83-1 --&gt;|subject of|190EA9377428B4CB
CHEBI:10057 --&gt;|object of|190EA9377428B4CB
bb1162fb2afb1c519c0aa8be98c352061720af220e2d052c571a1fecabff9800 --&gt;|has evidence|190EA9377428B4CB
cas:92-83-1 --&gt;|subject of|25AC613A93F7EF14
mesh:C002563 --&gt;|object of|25AC613A93F7EF14
84238c3cc875f0939ee4a8cf76bf33f216784434756833e3ba8d1b1183c372f5N --&gt;|has evidence|25AC613A93F7EF14
25AC613A93F7EF14 --&gt;|derived from|58f24ccfaf71431276da873c9e7b77ea61a2425e4e8b283b943542290deb292bN
25AC613A93F7EF14 --&gt;|derived from|bb1162fb2afb1c519c0aa8be98c352061720af220e2d052c571a1fecabff9800
</code></pre>

<details>
<summary>Source SSSOM TSV</summary>

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th>subject_id</th>
      <th>subject_label</th>
      <th>predicate_id</th>
      <th>predicate_modifier</th>
      <th>object_id</th>
      <th>object_label</th>
      <th>mapping_justification</th>
      <th>author_id</th>
      <th>mapping_source</th>
      <th>derived_from</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>chebi:10057</td>
      <td>9H-xanthene</td>
      <td>skos:exactMatch</td>
      <td>Not</td>
      <td>mesh:C002563</td>
      <td>xanthan gum</td>
      <td>semapv:ManualMappingCuration</td>
      <td>orcid:0000-0003-4423-4370</td>
      <td>wikidata:Q111239110</td>
      <td></td>
    </tr>
    <tr>
      <td>cas:92-83-1</td>
      <td>Xanthene</td>
      <td>skos:exactMatch</td>
      <td></td>
      <td>chebi:10057</td>
      <td>9H-xanthene</td>
      <td>semapv:ManualMappingCuration</td>
      <td>orcid:0000-0003-4423-4370</td>
      <td>wikidata:Q111239110</td>
      <td></td>
    </tr>
    <tr>
      <td>cas:92-83-1</td>
      <td>Xanthene</td>
      <td>skos:exactMatch</td>
      <td>Not</td>
      <td>mesh:C002563</td>
      <td>xanthan gum</td>
      <td>semapv:MappingChaining</td>
      <td></td>
      <td></td>
      <td>mapping:58f24ccfaf71431276da873c9e7b77ea61a2425e4e8b283b943542290deb292b~|mapping:bb1162fb2afb1c519c0aa8be98c352061720af220e2d052c571a1fecabff9800</td>
    </tr>
  </tbody>
</table>
</details>

<h2 id="background-knowledge">Background Knowledge</h2>

<p>Inference based on background knowledge was one of the key contributions of
SeMRA. For example, it’s known that mappings in ChEBI to CAS are exact matches,
but by curation convention, they’re annotated with the less precise
<code class="language-plaintext highlighter-rouge">oboInOwl:hasDbXref</code>. Workflows that incorporate this knowledge should be tagged
with <code class="language-plaintext highlighter-rouge">semapv:BackgroundKnowledgeBasedMatching</code> as a mapping justification.</p>

<p>In this example, I show how a poorly specified mapping from ChEBI is upgraded to
an exact match.</p>

<pre><code class="language-mermaid">flowchart LR
    CHEBI:10057[9H-xanthene
CHEBI:10057]
cas:92-83-1[Xanthene
cas:92-83-1]
964DD2FDA95501A2("CHEBI:10057
oboInOwl:hasDbXref
cas:92-83-1
unspecified")
style 964DD2FDA95501A2 fill: #bbf
obo:chebi[ChEBI Ontology
obo:chebi]
style obo:chebi fill: #feb
887c2cc0c006b49df5fa0bc281e23bd3722880d5096e27218082bd6edf96f59e[["CHEBI:10057
oboInOwl:hasDbXref
cas:92-83-1"]]
style 887c2cc0c006b49df5fa0bc281e23bd3722880d5096e27218082bd6edf96f59e fill: #f9f
B5CF0F3AB755AC6D("CHEBI:10057
skos:exactMatch
cas:92-83-1
background know.")
style B5CF0F3AB755AC6D fill: #bbf
e61f33dbb925f2282823afdad56c24feee9875953ea2de9124a50e47bd63418a[["CHEBI:10057
skos:exactMatch
cas:92-83-1"]]
style e61f33dbb925f2282823afdad56c24feee9875953ea2de9124a50e47bd63418a fill: #f9f
964DD2FDA95501A2 --&gt;|source|obo:chebi
CHEBI:10057 --&gt;|subject of|964DD2FDA95501A2
cas:92-83-1 --&gt;|object of|964DD2FDA95501A2
887c2cc0c006b49df5fa0bc281e23bd3722880d5096e27218082bd6edf96f59e --&gt;|has evidence|964DD2FDA95501A2
CHEBI:10057 --&gt;|subject of|B5CF0F3AB755AC6D
cas:92-83-1 --&gt;|object of|B5CF0F3AB755AC6D
e61f33dbb925f2282823afdad56c24feee9875953ea2de9124a50e47bd63418a --&gt;|has evidence|B5CF0F3AB755AC6D
B5CF0F3AB755AC6D --&gt;|derived from|887c2cc0c006b49df5fa0bc281e23bd3722880d5096e27218082bd6edf96f59e
</code></pre>

<details>
<summary>Source SSSOM TSV</summary>

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th>subject_id</th>
      <th>subject_label</th>
      <th>predicate_id</th>
      <th>predicate_label</th>
      <th>object_id</th>
      <th>object_label</th>
      <th>mapping_justification</th>
      <th>mapping_source</th>
      <th>derived_from</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>chebi:10057</td>
      <td>9H-xanthene</td>
      <td>oboInOwl:hasDbXref</td>
      <td>has database cross-reference</td>
      <td>cas:92-83-1</td>
      <td>Xanthene</td>
      <td>semapv:UnspecifiedMatching</td>
      <td>obo:chebi</td>
      <td></td>
    </tr>
    <tr>
      <td>chebi:10057</td>
      <td>9H-xanthene</td>
      <td>skos:exactMatch</td>
      <td></td>
      <td>cas:92-83-1</td>
      <td>Xanthene</td>
      <td>semapv:BackgroundKnowledgeBasedMatching</td>
      <td></td>
      <td>mapping:887c2cc0c006b49df5fa0bc281e23bd3722880d5096e27218082bd6edf96f59e</td>
    </tr>
  </tbody>
</table>
</details>

<h2 id="end-to-end-inference">End-to-End Inference</h2>

<p>This end-to-end example demonstrates the cumulation of inversion, chaining, and
background-based workflows. While it only starts with two mappings, it shows how
the successive application of these workflows can give full transparency and
auditability in the process of inference.</p>

<p>In real-world scenarios, I would want to use the mesh-cas exact match to
automatically integrate data, such as in the construction of a knowledge graph.
In case this mapping is adjacent to a node important for a prediction, I would
want to be able to fully audit how that node was constructed via this mapping
diagram.</p>

<pre><code class="language-mermaid">flowchart LR
    CHEBI:133530[tyramine sulfate
CHEBI:133530]
cas:30223-92-8[Tyramine sulfate
cas:30223-92-8]
983062C7B8D6517E("CHEBI:133530
oboInOwl:hasDbXref
cas:30223-92-8
unspecified")
style 983062C7B8D6517E fill: #bbf
obo:chebi[ChEBI Ontology
obo:chebi]
style obo:chebi fill:#feb
0b8eb968c306d65e1715a7b0961f6a4d99b5b19081edb67cee701fd887af1290[["CHEBI:133530
oboInOwl:hasDbXref
cas:30223-92-8"]]
style 0b8eb968c306d65e1715a7b0961f6a4d99b5b19081edb67cee701fd887af1290 fill: #f9f
21056C0956788E11("CHEBI:133530
skos:exactMatch
cas:30223-92-8
background know.")
style 21056C0956788E11 fill: #bbf
a0022401f47964288ecc1ab706d79b4d4abc10edf33d0a71953834a0b0b3c24c[["CHEBI:133530
skos:exactMatch
cas:30223-92-8"]]
style a0022401f47964288ecc1ab706d79b4d4abc10edf33d0a71953834a0b0b3c24c fill: #f9f
mesh:C027957[tyramine O-sulfate
mesh:C027957]
244808D78AFCBDC1("CHEBI:133530
skos:exactMatch
mesh:C027957
manual curation")
style 244808D78AFCBDC1 fill: #bbf
orcid:0000-0003-4423-4370[Charles Tapley Hoyt
orcid:0000-0003-4423-4370]
style orcid:0000-0003-4423-4370 fill: #bef
wikidata:Q111239110[Biomappings
wikidata:Q111239110]
style wikidata:Q111239110 fill: #feb
b8d737b89a421bd6ca058314564c9ed507cbfe3ec4a2e82979fefdfe708019ea[["CHEBI:133530
skos:exactMatch
mesh:C027957"]]
style b8d737b89a421bd6ca058314564c9ed507cbfe3ec4a2e82979fefdfe708019ea fill: #f9f
474146D6A6AFF3C7("mesh:C027957
skos:exactMatch
CHEBI:133530
inversion")
style 474146D6A6AFF3C7 fill: #bbf
1036c55358639c5db78ada181ac38d8eda337e83efe1db901716d101777f8474[["mesh:C027957
skos:exactMatch
CHEBI:133530"]]
style 1036c55358639c5db78ada181ac38d8eda337e83efe1db901716d101777f8474 fill: #f9f
AED810A43159DA66("mesh:C027957
skos:exactMatch
cas:30223-92-8
chaining")
style AED810A43159DA66 fill: #bbf
b5cd9f2dfa98540a3485a473bed0870720d7de23b87847ba508b8c85961e3b7d[["mesh:C027957
skos:exactMatch
cas:30223-92-8"]]
style b5cd9f2dfa98540a3485a473bed0870720d7de23b87847ba508b8c85961e3b7d fill: #f9f
983062C7B8D6517E --&gt;|source|obo:chebi
CHEBI:133530 --&gt;|subject of|983062C7B8D6517E
cas:30223-92-8 --&gt;|object of|983062C7B8D6517E
0b8eb968c306d65e1715a7b0961f6a4d99b5b19081edb67cee701fd887af1290 --&gt;|has evidence|983062C7B8D6517E
CHEBI:133530 --&gt;|subject of|21056C0956788E11
cas:30223-92-8 --&gt;|object of|21056C0956788E11
a0022401f47964288ecc1ab706d79b4d4abc10edf33d0a71953834a0b0b3c24c --&gt;|has evidence|21056C0956788E11
244808D78AFCBDC1 --&gt;|has author|orcid:0000-0003-4423-4370
244808D78AFCBDC1 --&gt;|source|wikidata:Q111239110
CHEBI:133530 --&gt;|subject of|244808D78AFCBDC1
mesh:C027957 --&gt;|object of|244808D78AFCBDC1
b8d737b89a421bd6ca058314564c9ed507cbfe3ec4a2e82979fefdfe708019ea --&gt;|has evidence|244808D78AFCBDC1
mesh:C027957 --&gt;|subject of|474146D6A6AFF3C7
CHEBI:133530 --&gt;|object of|474146D6A6AFF3C7
1036c55358639c5db78ada181ac38d8eda337e83efe1db901716d101777f8474--&gt;|has evidence|474146D6A6AFF3C7
mesh:C027957--&gt;|subject of|AED810A43159DA66
cas:30223-92-8 --&gt;|object of|AED810A43159DA66
b5cd9f2dfa98540a3485a473bed0870720d7de23b87847ba508b8c85961e3b7d --&gt;|has evidence|AED810A43159DA66
21056C0956788E11--&gt;|derived from|0b8eb968c306d65e1715a7b0961f6a4d99b5b19081edb67cee701fd887af1290
474146D6A6AFF3C7 --&gt;|derived from|b8d737b89a421bd6ca058314564c9ed507cbfe3ec4a2e82979fefdfe708019ea
AED810A43159DA66 --&gt;|derived from|a0022401f47964288ecc1ab706d79b4d4abc10edf33d0a71953834a0b0b3c24c
AED810A43159DA66 --&gt;|derived from|1036c55358639c5db78ada181ac38d8eda337e83efe1db901716d101777f8474
</code></pre>

<details>
<summary>Source SSSOM TSV</summary>

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th>subject_id</th>
      <th>subject_label</th>
      <th>predicate_id</th>
      <th>predicate_label</th>
      <th>object_id</th>
      <th>object_label</th>
      <th>mapping_justification</th>
      <th>mapping_source</th>
      <th>derived_from</th>
      <th>author_id</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>chebi:133530</td>
      <td>tyramine sulfate</td>
      <td>oboInOwl:hasDbXref</td>
      <td>has database cross-reference</td>
      <td>cas:30223-92-8</td>
      <td>Tyramine sulfate</td>
      <td>semapv:UnspecifiedMatching</td>
      <td>obo:chebi</td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td>chebi:133530</td>
      <td>tyramine sulfate</td>
      <td>skos:exactMatch</td>
      <td></td>
      <td>cas:30223-92-8</td>
      <td>Tyramine sulfate</td>
      <td>semapv:BackgroundKnowledgeBasedMatching</td>
      <td></td>
      <td>mapping:0b8eb968c306d65e1715a7b0961f6a4d99b5b19081edb67cee701fd887af1290</td>
      <td></td>
    </tr>
    <tr>
      <td>chebi:133530</td>
      <td>tyramine sulfate</td>
      <td>skos:exactMatch</td>
      <td></td>
      <td>mesh:C027957</td>
      <td>tyramine O-sulfate</td>
      <td>semapv:ManualMappingCuration</td>
      <td>wikidata:Q111239110</td>
      <td></td>
      <td>orcid:0000-0003-4423-4370</td>
    </tr>
    <tr>
      <td>mesh:C027957</td>
      <td>tyramine O-sulfate</td>
      <td>skos:exactMatch</td>
      <td></td>
      <td>chebi:133530</td>
      <td>tyramine sulfate</td>
      <td>semapv:MappingInversion</td>
      <td></td>
      <td>mapping:b8d737b89a421bd6ca058314564c9ed507cbfe3ec4a2e82979fefdfe708019ea</td>
      <td></td>
    </tr>
    <tr>
      <td>mesh:C027957</td>
      <td>tyramine O-sulfate</td>
      <td>skos:exactMatch</td>
      <td></td>
      <td>cas:30223-92-8</td>
      <td>Tyramine sulfate</td>
      <td>semapv:MappingChaining</td>
      <td></td>
      <td>mapping:a0022401f47964288ecc1ab706d79b4d4abc10edf33d0a71953834a0b0b3c24c|mapping:1036c55358639c5db78ada181ac38d8eda337e83efe1db901716d101777f8474</td>
      <td></td>
    </tr>
  </tbody>
</table>
</details>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="SSSOM" /><category term="semantic mappings" /><category term="Biomappings" /><category term="ChEBI" /><category term="SEMAPV" /><category term="cas" /><category term="Medical Subject Headings" /><summary type="html"><![CDATA[The Simple Standard for Sharing Ontological Mappings (SSSOM) requires semantic mappings to annotate a justification from the Semantic Mapping Vocabulary (SEMAPV) such as semapv:ManualMappingCuration, semapv:MappingInversion, and semapv:MappingChaining. However, SSSOM did not have a mechanism for tracking which mappings were used during inference workflows like inversion or chaining. This post is about my recent SSSOM enhancement proposal to add a new field derived_from to support this.]]></summary></entry><entry><title type="html">Discussions at the 7th NFDI4Chem Consortium Meeting</title><link href="https://cthoyt.com/2026/05/18/nfdi4chem-7th-consortium-meeting.html" rel="alternate" type="text/html" title="Discussions at the 7th NFDI4Chem Consortium Meeting" /><published>2026-05-18T14:51:00+00:00</published><updated>2026-05-18T14:51:00+00:00</updated><id>https://cthoyt.com/2026/05/18/nfdi4chem-7th-consortium-meeting</id><content type="html" xml:base="https://cthoyt.com/2026/05/18/nfdi4chem-7th-consortium-meeting.html"><![CDATA[<p>The
<a href="https://nfdi4chem.de/event/consortium-meeting-7-0/">7<sup>th</sup> NFDI4Chem consortium meeting</a>
took place last week in Jena. This post is a summary of some of the interesting
discussions I had there.</p>

<h2 id="ontologies-and-mappings">Ontologies and Mappings</h2>

<p>I’ve been working on ontologies, semantic mappings, and knowledge graphs for
nearly a decade. These interests fit well within NFDI4Chem
<a href="https://nfdi4chem.de/your-nfdi4chem-team-get-to-know-the-consortium-4/">TA4</a>
and more generally within
<a href="https://github.com/nfdi-de/section-metadata-wg-onto/">NFDI Section Metadata’s Working Group on Ontologies and Mappings</a>.
I want to highlight a few discussions I had about these topics at the
7<sup>th</sup> NFDI4Chem consortium meeting:</p>

<h3 id="ontology-for-theoretical-chemistry">Ontology for Theoretical Chemistry</h3>

<p>I discussed with Mario Wolter and Philip Strömert on how we could support
Mario’s use case of making molecular dynamics and simulation experimental
metadata more fair by constructing an ontology for theoretical chemistry.</p>

<p>The <a href="https://semantic.farm/collection/0000014">NFDI4Chem Ontologies Collection</a>
on the Semantic Farm (adapted from
<a href="https://doi.org/10.1515/pac-2021-2007">Ontologies4Chem: the landscape of ontologies in chemistry</a>)
lists the Computational Chemistry Ontology from the World Avatar project, but
that resource is effectively unusable, motivating the curation of a new ontology
(which when applicable can map back to the World Avatar ontology).</p>

<p>We plan to use the
<a href="https://incatools.github.io/ontology-development-kit/">Ontology Development Kit (ODK)</a>
to begin work and will first focus on capturing basis sets used in computation.
The ODK supports us in curating mappings to the World Avatar project’s ontology
(when applicable) in the
<a href="https://mapping-commons.github.io/sssom/">Simple Standard for Sharing Ontological Mappings (SSSOM)</a>.
Interestingly, in the week since the conference, we identified parallel work in
the NFDI on the
<a href="https://github.com/CPCLab/molsim-ontology">Molecular Simulation Ontology (MOLSIM)</a>
which will need to be considered further.</p>

<h3 id="semantic-mappings-for-chmo">Semantic Mappings for CHMO</h3>

<p>In the weeks leading up to the consortium meeting, I have been discussing with
Philip Strömert how to leverage
<a href="https://github.com/cthoyt/sssom-curator/">SSSOM Curator</a> to predict and curate
semantic mappings between CHMO, FIX, REX, IUPAC OrangeBook, IUPAC GoldBook, and
DAPHNE4NFDI’s PaNET ontology. While his HiWi made initial mappings between CHMO
and GoldBook (among other exciting improvements) in
<a href="https://github.com/NFDI4Chem/rsc-cmo/pull/17">NFDI4Chem/rsc-cmo#17</a>, I was able
to prepare mappings at scale between all resources using SSSOM Curator, then
curate them relatively quickly (within a few hours) in
<a href="https://github.com/nfdi-de/section-metadata-wg-onto/pull/88">nfdi-de/section-metadata-wg-onto#88</a>
and
<a href="https://github.com/nfdi-de/section-metadata-wg-onto/pull/89">nfdi-de/section-metadata-wg-onto#89</a>.
Philip and I coordinated next steps for the ontology agenda for NFDI4Chem (TA4),
which will include porting several other chemistry ontologies to using the ODK
(RXNO, CHEMINF) and to write manuscript describing how to produce ontology
bridge files from the SSSOM semantic mapping curations.</p>

<h3 id="annotating-interfacial-electrochemistry-data">Annotating Interfacial Electrochemistry Data</h3>

<p><a href="https://orcid.org/0000-0002-9686-3948">Albert Engstfeld</a> presented
<a href="https://www.echemdb.org/">echemdb</a>, a community project for interfacial
electrochemistry and local research data management. They recently ported their
data model to <a href="https://linkml.io/">LinkML</a>, but weren’t aware of how ontologies
could be used to annotate data. I discussed with him how their free-text
descriptions of electrode types could be replaced with references to terms in
the <a href="https://semantic.farm/chmo">Chemical Methods Ontology (CHMO)</a> under the
<a href="http://purl.obolibrary.org/obo/CHMO_0002344"><code class="language-plaintext highlighter-rouge">CHMO:0002344</code> (electrode)</a>
hierarchy.</p>

<p>Immediately, Albert identified missing electrode terms from CHMO and places
where the terms could be better organized. Luckily, NFDI4Chem is currently under
the process of taking stewardship over CHMO and making a major update, so
NFDI4Chem will be able to support and enable Albert to improve the ontology to
better suit his data resource. In general, this shows the power of the
<a href="https://www.nature.com/articles/s41597-024-03406-w">open data, open code, open infrastructure (O3)</a>
mindset codified by the
<a href="https://academic.oup.com/database/article/doi/10.1093/database/baab069/6410158">OBO Foundry Principles</a>
that influenced the original development of CHMO.</p>

<h3 id="knowledge-graphs-and-graph-machine-learning">Knowledge Graphs and Graph Machine Learning</h3>

<p>I discussed with
<a href="https://www.ipoc.uni-stuttgart.de/pcmc/team/Endo/">Kenichi Endo</a> and
<a href="https://orcid.org/0009-0008-5367-2034">Felix Neubauer</a> (U. Stuttgart) how to
find ontologies to annotate their data describing reaction and process steps,
and ultimately how these could be converted into knowledge graphs. I also
discussed graph machine learning methods with them, particularly knowledge graph
embedding models (KGEMs) and the <a href="https://github.com/pykeen/pykeen/">PyKEEN</a>
graph machine learning library that I co-developed.</p>

<h3 id="better-ontological-communication">Better Ontological Communication</h3>

<p>On the train ride home, I sat with
<a href="https://orcid.org/0009-0004-4064-6065">Theo Bender</a> and reflected on the
discussions with Albert, Kenichi, and many others. Despite NFDI4Chem being a
nexus of ontology expertise, it’s actually the case that many people don’t
really know what they are, how to find relevant ontologies for their work, or
how to use them. This is an opportunity for me to participate in
<a href="https://nfdi4chem.de/your-nfdi4chem-team-get-to-know-the-consortium-5/">TA5</a> to
help improve the educational material NFDI4Chem has, and a call to give more
(less technical talks) demonstrating the usage of ontologies in NFDI4Chem to
motivate others to get excited (tracked in
<a href="https://github.com/NFDI4Chem/knowledge_base/issues/508">NFDI4Chem/knowledge_base#508</a>).
Further, this ties into discussions about Semantic Farm (see below) and how it’s
a tool that can help NFDI4Chem consortium members find the right ontologies for
their work.</p>

<h2 id="training-materials-and-dalia">Training Materials and DALIA</h2>

<p>On the train ride home, I also discussed with
<a href="https://orcid.org/0009-0009-4519-1959">Hans-Georg Weinig</a> (GDCh, German
Chemical Society) how NFDI4Chem could incorporate training materials from the
German Chemical Society into <a href="https://search.dalia.education/basic/">DALIA</a>, the
NFDI’s search portal for training materials, as part of TA5.</p>

<h2 id="semantic-farm">Semantic Farm</h2>

<p>The <a href="https://semantic.farm">Semantic Farm</a> (previously called the Bioregistry,
now adapted to be domain-agnostic) is a registry of ontologies, controlled
vocabularies, terminologies, and other resources that mint (persistent)
identifiers. While it’s already been widely adopted in the biomedical domain
since its creation in 2019 (e.g., by the OBO Foundry, Monarch Initiative), I’ve
been working to integrate it within NFDI4Chem and other NFDI consortia via the
<a href="https://github.com/nfdi-de/section-metadata-wg-onto/">NFDI Section Metadata Working Group for Ontology Harmonization and Mapping</a>.
I want to highlight a few discussions I had about the Semantic Farm at the
7<sup>th</sup> NFDI4Chem consortium meeting:</p>

<h3 id="extending-semantic-farms-provider-data-model">Extending Semantic Farm’s Provider Data Model</h3>

<p>For each ontology, controlled vocabulary, terminology, and other resources that
mint (persistent) identifiers, the Semantic Farm keeps track of one or more
websites that can provide information about entities from that resource. For
example, there is a first-party provider for the Gene Ontology that shows
information terms like <code class="language-plaintext highlighter-rouge">GO:0032571</code> (response to vitamin K). There are also many
third party providers such as through the EBI Ontology Lookup Service (OLS) and
Jackson Laboratories’ browser.</p>

<p>I talked with <a href="https://orcid.org/0000-0002-7899-7192">Steffen Neumann</a> about
extending the Semantic Farm’s data model for providers to include more
information about:</p>

<ol>
  <li>what media types (e.g., HTML, JSON, RDF) the provider returns by default</li>
  <li>whether content negotiation is possible by sending an <code class="language-plaintext highlighter-rouge">Accept</code> header to tell
the server which media type to return</li>
</ol>

<p>Steffen’s use case in NFDI4Chem
<a href="https://nfdi4chem.de/your-nfdi4chem-team-get-to-know-the-consortium-3/">TA3</a>/<a href="https://nfdi4chem.de/your-nfdi4chem-team-get-to-know-the-consortium/">TA6</a>
is the Semantic Farm entry for MassBank
(<a href="https://semantic.farm/massbank">https://semantic.farm/massbank</a>). Enabling
Semantic Farm to resolve in different ways will support the development of
computational workflows as well as make it more useful when implementing the
MassBank front-end.</p>

<h3 id="adding-semantic-web-interoperability-to-semantic-farms-resolver">Adding Semantic Web Interoperability to Semantic Farm’s Resolver</h3>

<p>I talked with <a href="https://egonw.github.io">Egon Willighagen</a> (a longtime
collaborator of mine and a member of one of the
<a href="https://nfdi4chem.de/the-advisory-boards/">NFDI4Chem advisory boards</a>) about
how to extend the “resolver” functionality of Semantic Farm to support content
negotiation. The resolver redirects URLs constructed with a CURIE like
https://semantic.farm/GO:0032571 to the first-party (or best) web page for human
reading.</p>

<p>Egon suggested that if a request contains an <code class="language-plaintext highlighter-rouge">Accept</code> header asking for
<code class="language-plaintext highlighter-rouge">text/turtle</code> (or any other RDF-adjacent mimetype) that it could return the list
of related URIs. Importantly, this functionality is already available via the
API
(<a href="https://semantic.farm/api/reference/GO:0032571">https://semantic.farm/api/reference/GO:0032571</a>)
and on the front-end
(<a href="https://semantic.farm/reference/GO:0032571">https://semantic.farm/reference/GO:0032571</a>),
but not possible through the resolver endpoint.</p>

<p>In
<a href="https://github.com/biopragmatics/bioregistry/pull/1954">biopragmatics/bioregistry#1954</a>,
I extended the resolver so now it’s possible to use <code class="language-plaintext highlighter-rouge">Accept</code> headers like in the
following:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">requests</span>

<span class="n">res</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span>
   <span class="s">"https://semantic.farm/GO:0032571"</span><span class="p">,</span>
   <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s">"Accept"</span><span class="p">:</span> <span class="s">"text/turtle"</span><span class="p">},</span>
<span class="p">)</span>
</code></pre></div></div>

<p>I also added a way of adding query parameters to get the same results when
navigating to
<a href="https://semantic.farm/GO:0032571?format=turtle">https://semantic.farm/GO:0032571?format=turtle</a>
(note the addition of <code class="language-plaintext highlighter-rouge">?format=turtle</code>). For both, the following is returned:</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@prefix</span><span class="w"> </span><span class="nn">rdfs:</span><span class="w"> </span><span class="nl">&lt;http://www.w3.org/2000/01/rdf-schema#&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="nl">&lt;https://semantic.farm/GO:0032571&gt;</span><span class="w"> </span><span class="nn">rdfs:</span><span class="n">seeAlso</span><span class="w"> </span><span class="nl">&lt;http://bio2rdf.org/go:0032571&gt;</span><span class="p">,</span><span class="w">
        </span><span class="nl">&lt;http://identifiers.org/obo.go/GO:0032571&gt;</span><span class="p">,</span><span class="w">
        </span><span class="nl">&lt;http://purl.obolibrary.org/obo/GO_0032571&gt;</span><span class="p">,</span><span class="w">
        </span><span class="nl">&lt;http://purl.org/obo/owl/GO#GO_0032571&gt;</span><span class="p">,</span><span class="w">
        </span><span class="nl">&lt;http://www.geneontology.org/GO:0032571&gt;</span><span class="p">,</span><span class="w">
        </span><span class="nl">&lt;http://www.informatics.jax.org/searches/GO.cgi?id=GO:0032571&gt;</span><span class="p">,</span><span class="w">
        </span><span class="nl">&lt;http://www.informatics.jax.org/vocab/gene_ontology/GO:0032571&gt;</span><span class="p">,</span><span class="w">
        </span><span class="nl">&lt;http://www.pantherdb.org/panther/category.do?categoryAcc=GO:0032571&gt;</span><span class="p">,</span><span class="w">
        </span><span class="nl">&lt;https://bioportal.bioontology.org/ontologies/GO/?p=classes&amp;conceptid=http://purl.obolibrary.org/obo/GO_0032571&gt;</span><span class="p">,</span><span class="w">
        </span><span class="nl">&lt;https://identifiers.org/GO:0032571&gt;</span><span class="p">,</span><span class="w">
        </span><span class="nl">&lt;https://n2t.net/go:0032571&gt;</span><span class="p">,</span><span class="w">
        </span><span class="nl">&lt;https://www.ebi.ac.uk/QuickGO/GTerm?id=GO:0032571&gt;</span><span class="p">,</span><span class="w">
        </span><span class="nl">&lt;https://www.ebi.ac.uk/QuickGO/term/GO:0032571&gt;</span><span class="p">,</span><span class="w">
        </span><span class="nl">&lt;https://www.ebi.ac.uk/ols4/ontologies/go/terms?iri=http://purl.obolibrary.org/obo/GO_0032571&gt;</span><span class="p">,</span><span class="w">
        </span><span class="nl">&lt;https://www.nextprot.org/term/GO:0032571&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<h3 id="semantic-farm-and-base4nfdi">Semantic Farm and Base4NFDI</h3>

<p>I’ve been working towards proposing the Semantic Farm as a Base4NFDI service
(despite the recent
<a href="https://all-chat.nfdi.de/channel/base4nfdi-general?msg=EMuGhpddQqfL748oM">announcement</a>
that Base4NFDI won’t be accepting any new proposals for funding). I discussed
this with <a href="https://orcid.org/0000-0002-1213-5135">Martin Reinhardt</a> and
<a href="https://orcid.org/0000-0003-2285-3322">Hannah Butz</a> from Base4NFDI at their
poster (photo below borrowed from Martin’s
<a href="https://www.linkedin.com/posts/martin-reinhardt_zaf-base4nfdi-nfdi-activity-7460274633150218241-OsNv">post on LinkedIn</a>).</p>

<p><img src="/img/nfdi4chem-7-base4nfdi-poster.jpg" alt="" /></p>

<p>They had the exciting idea to encourage people to draw on their poster, so I
added the relationships between TS4NFDI, KG4NFDI, and PID4NFDI and the Semantic
Farm. It’s possible to see in the photo if you squint, but here’s the same
arrows drawn again:</p>

<pre><code class="language-mermaid">flowchart LR
    ts4nfdi[TS4NFDI] --&gt;|already uses|sf[Semantic Farm]
    kgi4nfdi[KGI4NFDI] --&gt;|should use|sf
    pid4nfdi[PID4NFDI] --&gt;|could use|sf
</code></pre>

<p>We discussed how the Semantic Farm is already a key service as part of the
TS4NFDI, how it should be adopted by KGI4NFDI (e.g., to make sure that all NFDI
knowledge graphs use the same prefixes, CURIEs, and URIs for the same things),
and how could fit in with PID4NFDI to support them in better communicating what
identifiers do, where they come from, and how they make data FAIR beyond the
limited number of identifier spaces (e.g., DOI, ORCiD, ROR, Handles) on which
they currently focus. They also suggested I attend the
<a href="https://base4nfdi.de/?view=article&amp;id=152:save-the-date-uc4b-2026-in-berlin&amp;catid=8">Base4NFDI User Conference 2026</a>
in Berlin to communicate these things further.</p>

<h2 id="chemotion">Chemotion</h2>

<p>I’m currently leading the effort to develop a data infrastructure for the
<a href="https://catalaix.com/en">Catalaix</a> project which will capture experimental
information about polymerization and depolymerization reactions from both the
literature and experiments in our laboratory. Our researchers are using
Chemotion, so I discussed requirements for programmatically extracting data from
Chemotion with <a href="https://harivyasi.com">Shashank Harivyasi</a> (KIT), the Chemotion
developer responsible for Chemotion’s API. I also discussed with
<a href="https://orcid.org/0000-0001-9513-2468">Nicole Jung</a> (KIT) our more general need
to identify and reuse, or develop, an external standard for reaction
information.</p>

<hr />

<p>I’m a big fan of these meetings, especially because they are typically short and
well-attended. While this post focused on bigger picture discussions, I also had
lots of small talks with friends and collaborators like
<a href="https://orcid.org/0000-0003-1066-7792">Kohulan</a> and
<a href="https://orcid.org/0000-0002-2564-3243">Chandu</a> that were completely invaluable
to making NFDI4Chem a great experience. I’m looking forward to our next big
in-person meetup at Ontologies4Chem in Limburg in November!</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="NFDI" /><category term="NFDI4Chem" /><category term="chemistry" /><category term="SSSOM" /><category term="Chemotion" /><category term="semantic mappings" /><summary type="html"><![CDATA[The 7th NFDI4Chem consortium meeting took place last week in Jena. This post is a summary of some of the interesting discussions I had there.]]></summary></entry><entry><title type="html">International Society of Biocuration Presents: Curate This!</title><link href="https://cthoyt.com/2026/03/03/curate-this.html" rel="alternate" type="text/html" title="International Society of Biocuration Presents: Curate This!" /><published>2026-03-03T10:13:00+00:00</published><updated>2026-03-03T10:13:00+00:00</updated><id>https://cthoyt.com/2026/03/03/curate-this</id><content type="html" xml:base="https://cthoyt.com/2026/03/03/curate-this.html"><![CDATA[<p>While researchers typically communicate their work through poster presentations,
oral presentations, and written communication, programmers often give (live)
demonstrations. I’m not aware of any technical nor practical barriers for why
curators couldn’t do the same, and always wished that curators did this more
often. This post is about how I planned to make this a reality by starting a
podcast with the
<a href="https://www.biocuration.org">International Society for Biocuration (ISB)</a>
entitled
<a href="https://youtube.com/playlist?list=PLYM0tkKvhlX54EMQGIbAOOhMKlDDPF21P&amp;si=7AO9ur6yWiFHhtzL">ISB Presents: Curate This!</a>.</p>

<p>The key first step was to decide on the goals of the podcast and its target
audience. The primary goal of <em>Curate This!</em> is to explicitly show the process
of curation and have an informal discussion about the challenges associated with
it. It should have short episodes that require as little preparatory work for
both the interviewer and interviewee as possible to make it possible to scale. I
also decided with the ISB that it should be hosted as an ISB podcast, not as
something just from me. This better fits the message for the curation community
and is overall a better governance decision to support longevity (if we’re
successful).</p>

<p>It’s not a goal of this podcast to give a background on curation - there are
plenty of
<a href="https://www.biocuration.org/home-3/isb-publications/">resources available from the ISB</a>
that cover this. It’s also not a goal of this podcast to focus on the curator
themselves, such as how they became a curator - the ISB hosts two <em>Careers in
Biocuration</em> sessions each year, one at the in-person conference and one
virtually, that cover this. The target audience for this podcast is
practitioners.</p>

<h2 id="script">Script</h2>

<p><em>Curate This!</em> interviews are split into several segments. The first and last
are recorded by the interviewer after the interview is done to give an
introduction and parting remarks. The bulk of the episode is contained within
three segments: introduction, demonstration, and reflections.</p>

<p>I’ve written out the questions and the concept for each segment below to serve
as a resource for potential interviewees to read ahead of time and prepare
themselves as well as a resource for interviewers to follow and stay on task.</p>

<h3 id="introduction">Introduction</h3>

<p>The goal of the first segment of the interview is to describe the history,
goals, and uses of your curated resource in around five minutes. We’ll loosely
use the following question lists:</p>

<ul>
  <li>Basic
    <ul>
      <li>What is your resource called (if not already mentioned in the introduction)?</li>
      <li>When was your resource established?</li>
      <li>What kind of information is in your resource?</li>
      <li>Do you develop/reuse any data standards?</li>
    </ul>
  </li>
  <li>Impact
    <ul>
      <li>Who uses (or could use) your resource and why? How do you assess this?</li>
      <li>Have you seen any cool citations of your resource?</li>
      <li>What is the broader impact in the basic and translational research space in
biomedicine (or beyond?)</li>
    </ul>
  </li>
  <li>Personnel
    <ul>
      <li>What does your resource’s team look like?
        <ul>
          <li>How many people/groups work on your resource?</li>
          <li>Is it developed and maintained by a group within your institution, as a
community effort, or somewhere in between?</li>
        </ul>
      </li>
      <li>If it’s a community effort, How do you do project management and
communication? E.g., Slack, GitHub, Trello, etc.</li>
      <li>How do you onboard new curators? If there’s a difference between
internal/external, what does this dichotomy look like?</li>
    </ul>
  </li>
</ul>

<h3 id="demonstration">Demonstration</h3>

<p>The goal of the second segment of the interview is to demonstrate the
contribution of a curation to your resource, live, in between ten and thirty
minutes. Here’s what makes a satisfying live demonstration:</p>

<ul>
  <li>Show how you select what you’re going to curate.
    <ul>
      <li>How do you find content? For example, if you curate text from literature or
patents, do you have a search query that runs on a chronological basis?</li>
      <li>How do you prioritize content? For example, do you use ranking from a search
system, or a more sophisticated document classifier?</li>
    </ul>
  </li>
  <li>What do you look for in the text?
    <ul>
      <li>Do you use external ontologies, terminologies, or semantic spaces to tag
named entities?</li>
    </ul>
  </li>
  <li>What kinds of assumptions do you make as a curator? For example, if you’re
curating relationships between proteins, do you assume that authors refer to
proteins using their corresponding gene names?</li>
  <li>How do you report the confidence of your curation (and its components)?</li>
  <li>What kind of metadata do you capture, e.g., the curator’s ORCiD, the time of
curation, or anything else?</li>
</ul>

<p>Ideally, you should prepare a curation ahead of time so you can quickly walk
through the process during the live demo, rather than needing time to think and
consider (though, this might be more realistic!).</p>

<h3 id="reflections">Reflections</h3>

<p>The goal of the third segment of the interview is to reflect on the
demonstration and conclude the interview with parting thoughts, in around five
minutes.</p>

<ul>
  <li>Next Steps
    <ul>
      <li>What happens next after curation?
        <ul>
          <li>Does the data get reflected on the website immediately?</li>
          <li>Does a second curator check things?</li>
          <li>Are more substantial releases made periodically?</li>
        </ul>
      </li>
      <li>What are some difficulties/challenges in curating your resource?</li>
      <li>What could authors/journals/publishers do to make it easier to curate?
        <ul>
          <li>What data should they include (that they don’t)?</li>
          <li>How should data look?</li>
          <li>What kinds of standards would you like to see developed?</li>
        </ul>
      </li>
      <li>Contrast curating a “good” paper versus a “bad” paper.</li>
    </ul>
  </li>
  <li>Longevity and Sustainability
    <ul>
      <li>How/where do you think that AI has a place in the curation and maintenance
of your resource?</li>
      <li>What’s the funding situation like?</li>
      <li>How much do you estimate it costs to maintain this resource per year?</li>
    </ul>
  </li>
</ul>

<h2 id="first-episode">First episode</h2>

<p>In our inaugural episode, I interviewed Dr. Susan (Sue) Bello, a curator for the
<a href="https://www.informatics.jax.org/">Mouse Genome Informatics (MGI)</a> knowledge
base and <a href="https://www.alliancegenome.org/">Alliance of Genome Resources (AGR)</a>
who works at the Jackson Laboratory in Maine. Sue is also the ISB executive
committee chair. She showed us how she curates alleles in MGI using the paper
<a href="https://doi.org/10.1016/j.isci.2024.111587">Mice deficient in TWIK-1 are more susceptible to kainic acid-induced seizures</a>
(Kim <em>et al.</em>, 2025).</p>

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/rNwvZ9KhfCM?si=Kt16Y_aWV24sqkBQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<h2 id="let-us-interview-you">Let us Interview You</h2>

<p>If you curate a resource and want to be featured on the podcast, please
<a href="https://forms.gle/5Q2DGjWgWTHYxa8dA">fill out this form</a>.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="International Society of Biocuration" /><category term="curation" /><category term="biocuration" /><summary type="html"><![CDATA[While researchers typically communicate their work through poster presentations, oral presentations, and written communication, programmers often give (live) demonstrations. I’m not aware of any technical nor practical barriers for why curators couldn’t do the same, and always wished that curators did this more often. This post is about how I planned to make this a reality by starting a podcast with the International Society for Biocuration (ISB) entitled ISB Presents: Curate This!.]]></summary></entry><entry><title type="html">Efficient Bulk Access to Citations in OpenCitations</title><link href="https://cthoyt.com/2026/02/10/opencitations-client.html" rel="alternate" type="text/html" title="Efficient Bulk Access to Citations in OpenCitations" /><published>2026-02-10T09:46:00+00:00</published><updated>2026-02-10T09:46:00+00:00</updated><id>https://cthoyt.com/2026/02/10/opencitations-client</id><content type="html" xml:base="https://cthoyt.com/2026/02/10/opencitations-client.html"><![CDATA[<p><a href="https://opencitations.net">OpenCitations</a> aggregates and deduplicates
bibliographic information from CrossRef, Europe PubMed Central, and other
sources to construct a comprehensive, open index of citations between scientific
works. This post describes the
<a href="https://github.com/cthoyt/opencitations-client"><code class="language-plaintext highlighter-rouge">opencitations-client</code></a> package
which wraps the OpenCitations API and implements an automated pipeline for
locally downloading, caching, and accessing OpenCitations in bulk.</p>

<h2 id="background">Background</h2>

<p>OpenCitations both provides access via an <a href="https://api.opencitations.net">API</a>
and <a href="https://download.opencitations.net">bulk data downloads</a> distributed across
FigShare and Zenodo. Importantly, it publishes its data under the CC0 public
domain license to democratize access to citations - previously, this data was
only available through paid access to commercial databases owned by publishers.</p>

<p>While API access can be convenient for <em>ad-hoc</em> usage, it’s generally slow,
rate-limited, susceptible to DDoS (e.g., from crawlers), and therefore difficult
(if not impossible) to use in bulk. My solution is to write software that
automates downloading, processing, and caching databases in bulk and provides
fast, highly available, local access. I’ve previously written about developing
standalone software packages for several large databases including
<a href="/2020/12/14/taming-drugbank.html">DrugBank</a>,
<a href="/2021/08/05/taming-chembl-sql.html">ChEMBL</a>,
<a href="/2023/09/01/umls.html">UMLS</a>,
<a href="/2024/06/08/easy-orcid.html">ORCiD</a>, and
<a href="/2025/01/23/clinical-trials-data-modeling.html">ClinicalTrials.gov</a>.
Similarly, I maintain several similar workflows in the
<a href="https://github.com/biopragmatics/pyobo">PyOBO software package</a> for converting
resources into ontology-like data structures. I previously wrote about how this
looks for <a href="/2025/10/14/databases-as-ontologies-2-hgnc.html">HGNC</a>.</p>

<h2 id="building-on-an-existing-ecosystem">Building on an Existing Ecosystem</h2>

<p>I’ve been developing a software ecosystem over the last decade to support common
workflows in research data management and data integration. When I start a new
project, I try and reuse or improve existing components from that ecosystem
wherever possible. Importantly, I try and find meaningful ways of organizing
code across my ecosystem to reduce duplication, separate concerns, reduce the
burden of testing, and ease maintenance.</p>

<p>OpenCitations publishes its
<a href="https://download.opencitations.net/">bulk data dumps</a> across several records in
Figshare and Zenodo. I’ve previously written
<a href="https://github.com/cthoyt/zenodo-client/"> <code class="language-plaintext highlighter-rouge">zenodo-client</code></a> to interact with
Zenodo’s API and orchestrates downloading and caching. <code class="language-plaintext highlighter-rouge">zenodo-client</code> heavily
builds on <a href="https://github.com/cthoyt/pystow"><code class="language-plaintext highlighter-rouge">pystow</code></a>, which implements I/O and
filesystem operations to enable reproducible, automated downloading, caching,
and opening of data.</p>

<p>I had not previously written software to interact with Figshare, so I followed
the form of <code class="language-plaintext highlighter-rouge">zenodo-client</code> and created a new package,
<a href="https://github.com/cthoyt/figshare-client"><code class="language-plaintext highlighter-rouge">figshare-client</code></a>. I’m able to
quickly create new high-quality packages because I’ve encoded all the wisdom and
experience I’ve gained over the years in a Cookiecutter template,
<a href="https://github.com/cthoyt/cookiecutter-snekpack">cookiecutter-snekpack</a>, which
I can use to set up a new project in mere minutes.</p>

<p>Along the way, I realized that the archives in Zenodo and Figshare were a
combination of TAR and ZIP archives, each with many CSV files inside. In Python,
TAR and ZIP archives have lots of weird quirks, even though they mostly do the
same thing. However, rather than addressing those issues in
<code class="language-plaintext highlighter-rouge">opencitations-client</code>, it made more sense to add utility functions in PyStow in
<a href="https://github.com/cthoyt/pystow/pull/125">cthoyt/pystow#125</a> (tar and zip
archive iteration), which I was much better able to test in the PyStow archive.</p>

<p>A key functionality of OpenCitations is to implement graph-like queries to find
incoming and outgoing citations. I considered several solutions for efficiently
caching and querying graph-like data including pickles and SQLite, but these
were respectively slow and disk inefficient. I found better solutions based on
NumPy’s memory maps and was surprised that I couldn’t find an implementation in
a popular package (e.g., SciPy). So, I had to decide where to put an
implementation of disk-based cached graph. I didn’t want to put it in
OpenCitations nor make a tiny package for just this one operation, so I decided
to expand the scope of PyStow and add it there in
<a href="https://github.com/cthoyt/pystow/pull/121">cthoyt/pystow#121</a>.</p>

<p>Finally, OpenCitations deals with a variety of identifier spaces including
first-party <a href="https://semantic.farm/omid">OpenCitations Metadata IDs (OMIDs)</a> and
<a href="https://semantic.farm/oci">OpenCitations Citation IDs (OCIs)</a> as well as
third-party identifiers from Wikidata, OpenAlex, PubMed, DOI, and others. I’ve
written the <a href="https://github.com/biopragmatics/curies"><code class="language-plaintext highlighter-rouge">curies</code></a> to handle
identifiers in an explicit and transparent way. In the end, the
<code class="language-plaintext highlighter-rouge">opencitations-client</code> relies on several components from my ecosystem, and of
course, several more generic and popular packages. Here’s how the dependencies
look:</p>

<pre><code class="language-mermaid">flowchart LR
    opencitations-client -- depends on --&gt; figshare-client
    opencitations-client -- depends on --&gt; zenodo-client
    opencitations-client -- depends on --&gt; curies
    figshare-client -- depends on --&gt; pystow
    zenodo-client -- depends on --&gt; pystow
</code></pre>

<h2 id="demo">Demo</h2>

<p>It’s important for software packages to implement simple, top-level APIs that
cover 99% of use cases with reasonable defaults. Most use cases for
OpenCitations are to get incoming/outgoing citations for a DOI, PubMed
identifiers, or OpenCitations identifiers. Here’s how this looks:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">curies</span> <span class="kn">import</span> <span class="n">Reference</span>
<span class="kn">from</span> <span class="nn">opencitations_client</span> <span class="kn">import</span> <span class="n">get_incoming_citations</span><span class="p">,</span> <span class="n">get_outgoing_citations</span>

<span class="c1"># a CURIE for the DOI for the Bioregistry paper
</span><span class="n">bioregistry_curie</span> <span class="o">=</span> <span class="s">"doi:10.1038/s41597-022-01807-3"</span>

<span class="c1"># who did the Bioregistry paper cite?
</span><span class="n">outgoing</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Reference</span><span class="p">]</span> <span class="o">=</span> <span class="n">get_outgoing_citations</span><span class="p">(</span><span class="n">bioregistry_curie</span><span class="p">)</span>

<span class="c1"># who cited the Bioregistry paper?
</span><span class="n">incoming</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Reference</span><span class="p">]</span> <span class="o">=</span> <span class="n">get_incoming_citations</span><span class="p">(</span><span class="n">bioregistry_curie</span><span class="p">)</span>
</code></pre></div></div>

<p>Importantly, each of these functions has a <code class="language-plaintext highlighter-rouge">backend</code> argument that defaults to
<code class="language-plaintext highlighter-rouge">api</code> and can be swapped to <code class="language-plaintext highlighter-rouge">local</code>. Because everything is built on software
that is smart about caching, loading, and data workflows, on the first time
<code class="language-plaintext highlighter-rouge">backend='local'</code> is used, all processing happens automatically (warning, takes
a few hours on a single core). This function also has a <code class="language-plaintext highlighter-rouge">return_value</code> argument
that can be used to swap between principled <code class="language-plaintext highlighter-rouge">curies.Reference</code> data structures
that explicitly encode identifiers, simple string local unique identifiers that
match the input prefix, or full citation objects (only available through
OpenCitations API).</p>

<p>See the <code class="language-plaintext highlighter-rouge">opencitations-client</code> code on GitHub
(<a href="https://github.com/cthoyt/opencitations-client">https://github.com/cthoyt/opencitations-client</a>)
and documentation on ReadTheDocs
(<a href="https://opencitations-client.readthedocs.io">https://opencitations-client.readthedocs.io</a>).</p>

<hr />

<p>While I’ve been thinking about adding citations to the bibliographic components
of knowledge graph construction workflows for several years, I was finally
pushed to implement <code class="language-plaintext highlighter-rouge">opencitations-client</code> for the
<a href="https://catalaix.com">Catalaix project</a>, where we’re developing new methods for
recycling and reuse of (bio)plastics. I wanted to get all seventeen
laboratories’ publications, who they cited, and who cited them as a seed for
information extraction and curation. Here’s a small example of a citation
network from those queries:</p>

<pre><code class="language-mermaid">flowchart TD
    26802344["Mechanism-specific and whole-organism ecotoxicity of mono-rhamnolipids.
Blank (2016)"]
34492827["The Green toxicology approach: Insight towards the eco-toxicologically safe development of benign catalysts.
Herres-Pawlis (2021)"]
28779508["Highly Active N,O Zinc Guanidine Catalysts for the Ring-Opening Polymerization of Lactide.
Herres-Pawlis (2017)"]
33195133["Genetic Cell-Surface Modification for Optimized Foam Fractionation.
Blank (2020)"]
32974309["Integration of Genetic and Process Engineering for Optimized Rhamnolipid Production Using
Jupke, Blank (2020)"]
30811863["New Kids in Lactide Polymerization: Highly Active and Robust Iron Guanidine Complexes as Superior Catalysts.
Pich, Herres-Pawlis (2019)"]
30758389["Tuning a robust system: N,O zinc guanidine catalysts for the ROP of lactide.
Pich, Herres-Pawlis (2019)"]
28524364["Biofunctional Microgel-Based Fertilizers for Controlled Foliar Delivery of Nutrients to Plants.
Pich, Schwaneberg (2017)"]
34865895["A plea for the integration of Green Toxicology in sustainable bioeconomy strategies - Biosurfactants and microgel-based pesticide release systems as examples.
Pich, Blank, Schwaneberg (2022)"]
32449840["Robust Guanidine Metal Catalysts for the Ring-Opening Polymerization of Lactide under Industrially Relevant Conditions.
Herres-Pawlis (2020)"]
34492827 --&gt; 30811863
34492827 --&gt; 30758389
34492827 --&gt; 28779508
34492827 --&gt; 32449840
32974309 --&gt; 33195133
34865895 --&gt; 26802344
34865895 --&gt; 32974309
34865895 --&gt; 28524364
34865895 --&gt; 34492827
</code></pre>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="bibliometrics" /><category term="citations" /><category term="citation networks" /><summary type="html"><![CDATA[OpenCitations aggregates and deduplicates bibliographic information from CrossRef, Europe PubMed Central, and other sources to construct a comprehensive, open index of citations between scientific works. This post describes the opencitations-client package which wraps the OpenCitations API and implements an automated pipeline for locally downloading, caching, and accessing OpenCitations in bulk.]]></summary></entry><entry><title type="html">Challenges with Semantic Mappings</title><link href="https://cthoyt.com/2026/01/20/semantic-mapping-challenges.html" rel="alternate" type="text/html" title="Challenges with Semantic Mappings" /><published>2026-01-20T10:42:00+00:00</published><updated>2026-01-20T10:42:00+00:00</updated><id>https://cthoyt.com/2026/01/20/semantic-mapping-challenges</id><content type="html" xml:base="https://cthoyt.com/2026/01/20/semantic-mapping-challenges.html"><![CDATA[<p>There are many challenges associated with the curation, publication,
acquisition, and usage of semantic mappings. This post examines their
philosophical, technical, and practical implications, highlights existing
solutions, and describes opportunities for next steps for the community of
curators, semantic engineers, software developers, and data scientists who make
and use semantic mappings.</p>

<h3 id="proliferation-of-formats">Proliferation of Formats</h3>

<p>The first challenge with semantic mappings is the variety of forms they can
take. This both includes different data models and serializations of those
models. This problem is effectively solved, but I think is worth reviewing for
historical purposes (please let me know if I missed something):</p>

<p><img src="https://forge.extranet.logilab.fr/uploads/-/system/project/avatar/107/external-content.duckduckgo.com.jpeg" align="left" style="max-height: 3em;" alt="SKOS logo" />
<a href="https://www.w3.org/TR/skos-reference">Simple Knowledge Organization System (SKOS)</a>
is a data model for RDF to represent controlled vocabularies, taxonomies,
dictionaries, thesauri, and other semantic artifacts. It defines several
semantic mapping predicates including for broad matches, narrow matches, close
matches, related matches, and exact matches.</p>

<p><a href="https://gbv.github.io/jskos/#mapping">JSKOS (JSON for Knowledge Organization Systems)</a>,
a JSON-based extension of the SKOS data model. I recently wrote a post about
converting between <a href="/2026/01/15/sssom-to-jskos.html">SSSOM and JSKOS</a>.</p>

<p><img src="https://www.jean-delahousse.net/wp-content/uploads/2020/09/Owl_logo-258x300.png" align="left" style="max-height: 3em; margin-right: 0.5em;" alt="OWL logo" />
<a href="https://www.w3.org/TR/owl2-syntax/">Web Ontology Language (OWL)</a> is primarily
used for ontologies. It has first-class language support for encoding
equivalences between classes, properties, or individuals. Other semantic
mappings can be encoded as annotation properties on classes, properties, or
individuals, e.g., using SKOS predicates.</p>

<p><img src="https://obofoundry.org/images/foundrylogo.png" align="left" style="max-height: 3em; margin-right: 0.5em;" alt="OBO logo" />
The
<a href="https://owlcollab.github.io/oboformat/doc/GO.format.obo-1_4.html">OBO Flat File Format</a>
is a simplified version of OWL with macros most useful for curating biomedical
ontologies. It has the same abilities as OWL, but also the <code class="language-plaintext highlighter-rouge">xref</code> macro which
corresponds to <code class="language-plaintext highlighter-rouge">oboInOwl:hasDbXref</code> relations, which are by nature imprecise and
therefore used in a variety of ways.</p>

<p><img src="https://avatars.githubusercontent.com/u/77892844?v=4" align="left" style="max-height: 3em; margin-right: 0.5em;" alt="SSSOM logo" />
The
<a href="https://mapping-commons.github.io/sssom/">Simple Standard for Sharing Ontological Mappings (SSSOM)</a>
is a fit-for-purpose format for semantic mappings between classes, properties,
or individuals. SSSOM guides curators towards inputting key metadata that are
typically missing from other formalisms and is gaining wider community adoption.
Importantly, SSSOM integrates into ontology curation workflows, especially for
<a href="https://incatools.github.io/ontology-development-kit">Ontology Development Kit (ODK)</a>
users.</p>

<p>The
<a href="https://moex.gitlabpages.inria.fr/alignapi/edoal.html">Expressive and Declarative Ontology Alignment Language (EDOAL)</a>
lives in a similar space to SSSOM, but IMO was much less approachable (c.f.
XML + Java), and has not seen a lot of traction in the biomedical space.</p>

<p><img src="https://ontoportal.org/images/logo.png" align="left" style="max-height: 3em; margin-right: 0.5em;" alt="OntoPortal logo" />
<a href="https://ontoportal.org/">OntoPortal</a> has its own data model for semantic
mappings that has low metadata precision. I recently wrote a post on converting
<a href="/2025/11/23/sssom-from-bioportal.html">OntoPortal to SSSOM</a>. OntoPortal would also like
to invest more in SSSOM infrastructure if it can organize funding and human resources.</p>

<p><img src="https://upload.wikimedia.org/wikipedia/commons/6/66/Wikidata-logo-en.svg" align="left" style="max-height: 3em" alt="Wikidata logo" />
<a href="https://www.wikidata.org">Wikidata</a> has its own data model for semantic
mappings that include higher precision metadata. I recently wrote a post on
mapping between the data models from <a href="/2026/01/08/sssom-to-wikidata.html">SSSOM and
Wikidata</a>.</p>

<p>Finally, there’s a long tail of mappings that live in poorly annotated CSV, TSV,
Excel, and other formats. Similarly, mappings can live in plain RDF files, e.g.,
encoded with SKOS predicates, but without high precision metadata.</p>

<h3 id="scattered-partially-overlapping-and-incomplete">Scattered, Partially Overlapping, and Incomplete</h3>

<p>Semantic mappings are not centralized, meaning that multiple sources of semantic
mappings often need to be integrated to map between two semantic spaces. Even
then, these integrated mappings are often incomplete. Using
<a href="https://semantic.farm/mesh">Medical Subject Headings (MeSH)</a> and the
<a href="https://semantic.farm/hpo">Human Phenotype Ontology (HPO)</a> as an example, we
can see the following:</p>

<ol>
  <li>MeSH doesn’t maintain any mappings to HPO.</li>
  <li>HPO maintains some mappings as primary mappings.</li>
  <li>The <a href="https://semantic.farm/umls">Unified Medical Language System (UMLS)</a>
maintains some mappings as secondary mappings. HPO suggests using UMLS as a
supplementary mapping resource.</li>
  <li><a href="https://github.com/biopragmatics/biomappings">Biomappings</a> maintains some
community-curated mappings as secondary mappings.</li>
</ol>

<p><a href="https://github.com/biopragmatics/semra/blob/main/notebooks/umls-inference-analysis.ipynb"><img src="/img/mappings-are-hard/scattered.png" alt="" /></a></p>

<p>This actually might not be the best example - it would have been better to show
a pair of resources that both partially map to the other. When I first made this
chart, I had to engineer the UMLS inference by hand. Eventually, the need to
generalize this workflow led to the development of the
<a href="https://github.com/biopragmatics/semra">Semantic Mapping Reasoner and Assembler (SeMRA)</a>
Python package which does this automatically and at scale. The fact that there
were missing mappings that even UMLS inference couldn’t retrieve led to
establishing the <a href="https://github.com/biopragmatics/biomappings">Biomappings</a>
project for prediction and semi-automated curation of semantic mappings. The
underlying technology stack from Biomappings eventually got spun out to
<a href="https://github.com/cthoyt/sssom-curator">SSSOM Curator</a> and is now fully
domain-agnostic.</p>

<h3 id="different-precision-or-conflicts">Different Precision or Conflicts</h3>

<p>Another challenge with semantic mappings is when different resources have
different level of precision. In the example below, OrphaNet uses low-precision
mapping predicates (i.e., <code class="language-plaintext highlighter-rouge">oboInOwl:hasDbXref</code>) while MONDO uses high-precision
mapping predicates (i.e., <code class="language-plaintext highlighter-rouge">skos:exactMatch</code>). It makes sense to take the highest
quality mapping in this situation, but having a coherent software stack to do
this at scale was the big challenge (solved by SeMRA).</p>

<p><a href="https://docs.google.com/drawings/d/1jBK1-FxzfsBFd6Ro0YjQSvwJCZs1rqlLQq9FdtcEU-w/edit?usp=sharing"><img src="/img/mappings-are-hard/precision.svg" alt="" /></a></p>

<p>This can get a bit dicier when there might be conflicting information, for
example, if one resource says exact match and another says broader match. In
SeMRA, I devised a confidence assessment scheme (which should get its own post
later).</p>

<h3 id="common-conflations">Common Conflations</h3>

<p>There are three flavors of conflations that make curating and reviewing mappings
difficult that I want to highlight.</p>

<h4 id="different-ontology-encodings">Different Ontology Encodings</h4>

<p>Classes, instances, and properties are mutually exclusive by design. This means
that any semantic mappings between them are nonsense, but there are many
situations where these mappings might get produced by an automated system or by
a curator who is less knowledgable about the ontology aspect of semantic
mappings. There’s also a much more subtle discussion about classes, instances,
and metaclasses ( see
<a href="https://github.com/OBOFoundry/OBOFoundry.github.io/issues/2454">this discussion</a>)
that I would set aside.</p>

<p>As a concrete example, the
<a href="https://semantic.farm/registry/iao">Information Artifact Ontology (IAO)</a> has a
class that represents the section of a document that contains its abstract:
<a href="http://purl.obolibrary.org/obo/IAO_0000315">abstract (IAO:0000315)</a>. Schema.org
has an annotation property whose range is a creative work and whose domain is
the text of the abstract itself: <a href="http://schema.org/abstract">schema:abstract</a>.
These both have the same label <code class="language-plaintext highlighter-rouge">abstract</code>, which means that it’s possible to
conflate (i.e., accidentally map them).</p>

<h4 id="different-entity-types">Different Entity Types</h4>

<p>The second kind of conflation is even more subtle, when two classes, instances,
or properties come from similar but distinct hierarchies.</p>

<p>For example, there’s a subtle difference between what is a phenotype and what is
a disease. Ontologies are highly apt at encoding this subtlety with <em>axioms</em>
that can then be used by reasoners. This can become a problem for curating and
reviewing semantic mappings because some diseases are named after the phenotype
that it presents or that causes it. Using MeSH’s disease hierarchy and HPO’s
phenotype hierarchy as an example, we can see that
<a href="https://semantic.farm/mesh:D000069856">Staghorn Calculi (mesh:D000069856)</a> and
<a href="https://semantic.farm/hp:0033591">Staghorn calculus (HP:0033591)</a> should not
get mapped.</p>

<p>Many more examples can be produced (which also show there are even more
subtleties here) using SSSOM Curator with the command:
<code class="language-plaintext highlighter-rouge">sssom_curator predict lexical doid hp</code>. See the
<a href="https://sssom-curator.readthedocs.io/en/latest/projects.html#making-predictions">SSSOM Curator documentation</a>
for more information on the lexical matching workflow.</p>

<h4 id="different-senses">Different Senses</h4>

<p>The <a href="https://basic-formal-ontology.org">basic formal ontology (BFO)</a> is an
upper-level ontology that is used by many ontologies, including almost the
entire <a href="https://obofoundry.org">Open Biomedical Ontologies (OBO) Foundry</a>.
However, as Chris Mungall described in his blog post,
<a href="https://douroucouli.wordpress.com/2022/08/10/shadow-concepts-considered-harmful/">Shadow Concepts Considered Harmful</a>,
there are many different senses in which an entity can be described, each
falling under a different, mutually exclusive branch of BFO. The figure below,
from Chris’s post, represents different senses in which a human heart can be
described:</p>

<p><a href="https://douroucouli.wordpress.com/2022/08/10/shadow-concepts-considered-harmful/"><img src="/img/mappings-are-hard/mungalls-ontology-design-guidelines-12.png" alt="" /></a></p>

<p>This problem is particularly bad in disease modeling. Here are only a few
examples (of many more) that illustrate this:</p>

<ul>
  <li>the <a href="https://semantic.farm/ogms">Ontology for General Medical Science (OGMS)</a>
term for
<a href="http://purl.obolibrary.org/obo/OGMS_0000031">disease (OGMS:0000031)</a>, the
<a href="https://semantic.farm/efo">Experimental Factor Ontology (EFO)</a> term for
<a href="http://www.ebi.ac.uk/efo/EFO_0000408">disease (EFO:0000408)</a>,
<a href="https://semantic.farm/mondo">Monarch Disease Ontology (MONDO)</a> term for
<a href="http://purl.obolibrary.org/obo/MONDO_0000001">disease (MONDO:0000001)</a> is a
<a href="http://purl.obolibrary.org/obo/BFO_0000016">disposition (BFO:0000016)</a></li>
  <li>the
<a href="https://semantic.farm/gsso">Gender, Sex, and Sexual Orientation Ontology (GSSO)</a>
term for <a href="http://purl.obolibrary.org/obo/GSSO_000486">disease (GSSO:000486)</a>
is a <a href="http://purl.obolibrary.org/obo/BFO_0000015">process (BFO:0000015)</a></li>
  <li>the <a href="https://semantic.farm/doid">Human Disease Ontology (DOID)</a> informally
mentions that a disease is a disposition, but doesn’t make an ontological
commitment to BFO</li>
  <li>many more controlled vocabularies including NCIT, SNOMED-CT, and MI have their
own terms for diseases but don’t use BFO as an upper-level ontology nor are
constructed in a way conducive towards integration with other ontologies</li>
</ul>

<p>Schultz <em>et al.</em> (2011) proposed a way to formalize the connections between the
various senses for diseases in
<a href="https://link.springer.com/article/10.1186/2041-1480-2-S2-S6">Scalable representations of diseases in biomedical ontologies</a>.
However, the OBO community has yet to resolve the
<a href="https://github.com/OBOFoundry/COB/pull/226">long and taxing discussion</a> on how
to standardize disease modeling practices.</p>

<p>For semantic mappings, this becomes a problem because a reasoner will explode if
diseases under two different BFO branches get marked as equivalent, because the
BFO upper level terms are marked as disjoint - this is a feature, not a bug.
However, while useful for creating carefully constructed, logically
(self-)consistent descriptions of diseases, these modeling choices can be
confusing when curating or reviewing mappings. These modeling choices might not
be so important in downstream applications, such as assembling a knowledge graph
to support graph machine learning, where many different knowledge sources with
lower levels of accuracy and precision must be merged. In practice, I have
merged triples using conflicting senses for diseases in a useful way, without
issue.</p>

<h3 id="interpretation-is-important">Interpretation is Important</h3>

<p>While the last few examples were cautionary tales for when things (probably)
shouldn’t be mapped, the next examples are about when things (probably) should
be mapped.</p>

<h4 id="definitions">Definitions</h4>

<p>Here are three vocabularies’ terms for proteins and their textual definitions
(though, many more contain their own term for proteins):</p>

<table>
  <thead>
    <tr>
      <th>Entity</th>
      <th>Label</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://www.wikidata.org/wiki/Q8054">wikidata:Q8054</a></td>
      <td>protein</td>
      <td>biomolecule or biomolecule complex largely consisting of chains of amino acid residues</td>
    </tr>
    <tr>
      <td><a href="http://semanticscience.org/resource/SIO_010043">SIO:010043</a></td>
      <td>protein</td>
      <td>A protein is an organic polymer that is composed of one or more linear polymers of amino acids.</td>
    </tr>
    <tr>
      <td><a href="http://purl.obolibrary.org/obo/PR_000000001">PR:000000001</a></td>
      <td>protein</td>
      <td>An amino acid chain that is canonically produced <em>de novo</em> by ribosome-mediated translation of a genetically-encoded mRNA, and any derivatives thereof.</td>
    </tr>
  </tbody>
</table>

<p>As semantic mapping curator, we have two options:</p>

<ol>
  <li>We can reasonably assume that the intent from all three resources was to
represent the same thing, despite the definitions being quite different. This
assumption can be built on our prior knowledge about what a protein is, why
Wikidata, SIO, and PR exist, and then infer the intent of the term’s
definition’s author</li>
  <li>We can make a very literal reading of the definition and conclude that these
three terms represent very different things</li>
</ol>

<p>I think that the latter is really unconstructive for several reasons, but I have
worked with colleagues, especially from the linguistics background, who take
this approach. First, this is unconstructive because it means you’ll probably
never map anything.</p>

<p>Second, if you want to be rigorous, use an ontology formalism with proper
logical definitions. For example, the
<a href="https://semantic.farm/cl">Cell Ontology (CL)</a> exhaustively defines its cells
using appropriate logical axioms. However, this also has a caveat, that to make
mappings based on logical definitions, then the different modelers have to agree
on the same axioms and same modeling paradigm. As far as I know, there aren’t
any groups out there that use the same modeling paradigm that haven’t just
combine forces to work on the same resource. So we’re stuck back at option 1
either way :)</p>

<h4 id="context-sometimes-matters">Context Sometimes Matters</h4>

<p>In contrast to the discussion about mapping phenotypes and diseases, there are
context-dependent reasons to make semantic mappings, which can be illustrated in
biomedicine using genes and proteins. Let’s start with some definitions:</p>

<ol>
  <li><a href="http://purl.obolibrary.org/obo/SO_0000704">SO:0000704</a> A gene is a region of
a chromosome that encodes a transcript</li>
  <li><a href="http://purl.obolibrary.org/obo/PR_000000001">PR:000000001</a> A protein is a
chain of amino acids</li>
</ol>

<p>The biomedical literature often uses gene symbols to discuss the proteins they
encode. While this isn’t precise, it’s still useful in many cases. Therefore,
when reading the COVID-19 literature, you will likely see discussion of the
IL6-STAT cascade, where IL6 is the HGNC gene symbol for the Interleukin 6
protein. Most of the time, the HGNC approved gene symbol is an initialism or
other abbreviation of the protein, but this isn’t always the case.</p>

<p>Edit: Sue Bello pointed out that most journals enforce gene names being put in
italics (<em>IL6</em>) and proteins without italics, though this requires the author
and reader to know that distinction, as well as for formatting to be preserved,
which it often isn’t unless you’re reading the original PDF or publisher’s HTML.</p>

<p>Similar to the literature, many pathway databases that accumulate knowledge
about the processes and reactions in which proteins take part actually use gene
symbols (or other gene identifiers) to curate proteins.</p>

<p><img src="/img/mappings-are-hard/context-dependent.svg" alt="" /></p>

<p>The take-home message here is that genes and proteins are indeed not the same
thing, but in some contexts, it’s useful to map between them. There’s also a
compromise - the <a href="https://semantic.farm/ro">Relation Ontology (RO)</a> has a
predicate <a href="https://semantic.farm/RO:0002205">has gene product (RO:0002205)</a> that
explicitly models the relationship between IL6 and Interleukin 6, which can then
be automatically inferred to mean a less precise mapping for certain scenarios
(SeMRA implements this).</p>

<p>Outside of biomedicine, I have also heard that context-specific mappings are
very important in the digital humanities. As I’m better understanding the use
cases of colleagues in other NFDI Consortia that focus on the digital
humanities, I will try and update this section to have alternate perspectives.</p>

<h3 id="evidence">Evidence</h3>

<p>A key challenge that motivated the development of SSSOM as a standard was to
associate high-quality metadata with semantic mappings, such as the reason the
mapping was produced (e.g., manual curation, lexical matching, structural
matching), who produced it (e.g., a person, algorithm, agent), when, how, and
more.</p>

<p><a href="https://docs.google.com/drawings/d/1rBofcaQxBFuYX0OzhCvBkigSNFWLclAbQ_X7zG7PRKA/edit?usp=sharing"><img src="/img/mappings-are-hard/evidence.svg" alt="" /></a></p>

<p>We developed the
<a href="https://semantic.farm/registry/semapv">Semantic Mapping Vocabulary (semapv)</a> to
encode different kinds of evidence such as for manual curation of mappings,
lexical matching, structural matching, and others. SSSOM is well-suited towards
capturing simple evidences (blue).</p>

<h4 id="provenance-for-inferences">Provenance for Inferences</h4>

<p>The purple evidence from the figure in the last section requires a more detailed
data model to represent provenance for inferred semantic mappings that simply
doesn’t fit in the SSSOM paradigm (and it shouldn’t be hacked in, either). I
proposed a more detailed data model for capturing how inference is done in
<a href="https://doi.org/10.1093/bioinformatics/btaf542">Assembly and reasoning over semantic mappings at scale for biomedical data integration</a>
and provided a reference implementation in the
<a href="https://github.com/biopragmatics/semra">Semantic Reasoning Mapper and Reasoner (SeMRA)</a>
Python software package. Here’s what that data model looks like, which also has
a Neo4j counterpart:</p>

<p><a href="https://docs.google.com/drawings/d/1C5l1UmwKohMsgprSXRK6Lo2egLsRWhXPIfoVo09tJ9I/edit?usp=sharing"><img src="/img/mappings-are-hard/semra-data-model.svg" alt="" /></a></p>

<h3 id="negative-semantic-mappings">Negative Semantic Mappings</h3>

<p>SSSOM also has first-class support for encoding <em>negative</em> relationships,
meaning that the following can be represented:</p>

<p><a href="https://docs.google.com/drawings/d/1AfCR35ra3FyQMulaTlynVZKswLbj8gp5MA4N2ipFe1I/edit?usp=sharing"><img src="/img/mappings-are-hard/negatives.svg" alt="" /></a></p>

<p>This means that SSSOM curators can keep track of non-trivial negative mappings,
e.g., when curating the results of semantic mapping prediction or automated
inference. In a semi-automated curation loop, this allows us to avoid
re-reviewing <a href="https://doi.org/10.32388/DYZ5J3">zombie mappings</a> over and over
again.</p>

<p>High quality, non-trivial negative mappings also enable more accurate machine
learning, as opposed to using negative sampling. For example, we have been
working on developing graph machine learning-based ontology matching and merging
using <a href="https://github.com/pykeen/pykeen/">PyKEEN</a> (a graph machine learning
package I helped develop and maintain).</p>

<p>An open challenge is that we neither have support from data modeling formalisms
(e.g., ontologies in OWL, knowledge graphs in RDF or Neo4j) to encode negative
knowledge (in this case negative mappings) nor tooling support. This means that
when we output SSSOM to RDF, we use our own formalism, which won’t be correctly
recognized by any other tooling that wasn’t developed with SSSOM in mind. I’m
keeping notes about this in a separate <a href="/2025/10/07/negative-rdf.html">post about negative
knowledge</a> that I update periodically.</p>

<hr />

<p>Despite the challenges, I think that the mapping world is actually getting quite
mature. I am currently working with NFDI and RDA colleagues to further unify the
SSSOM and JSKOS worlds, especially given that the
<a href="https://coli-conc.gbv.de/cocoda/">Cocoda</a> mapping curation tool solved many of
these problems (from the digital humanities perspective) many years ago, and we
simply were unaware of it.</p>

<p>I hope this post can continue as a living document - if I missed something,
please let me know and I will update the post to include it!</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="SSSOM" /><category term="semantic mappings" /><category term="knowledge graphs" /><summary type="html"><![CDATA[There are many challenges associated with the curation, publication, acquisition, and usage of semantic mappings. This post examines their philosophical, technical, and practical implications, highlights existing solutions, and describes opportunities for next steps for the community of curators, semantic engineers, software developers, and data scientists who make and use semantic mappings.]]></summary></entry><entry><title type="html">Semantic Mappings Enable Automated Assembly</title><link href="https://cthoyt.com/2026/01/16/mappings-for-automated-assembly.html" rel="alternate" type="text/html" title="Semantic Mappings Enable Automated Assembly" /><published>2026-01-16T10:42:00+00:00</published><updated>2026-01-16T10:42:00+00:00</updated><id>https://cthoyt.com/2026/01/16/mappings-for-automated-assembly</id><content type="html" xml:base="https://cthoyt.com/2026/01/16/mappings-for-automated-assembly.html"><![CDATA[<p>Data and knowledge originating from heterogeneous sources often use
heterogeneous controlled vocabularies and/or ontologies for annotating named
entities. Semantic mappings are essential towards resolving these discrepancies
and integrating in a coherent way. This post highlights how this looks in two
scenarios: when constructing a knowledge graph for graph machine learning and
when constructing a comprehensive lexica for natural language processing, text
mining, and curation.</p>

<h2 id="background">Background</h2>

<p>Data and knowledge integration are challenging because there exist many
controlled vocabularies, ontologies, taxonomies, thesauri, classifications, and
other resources that mint identifiers with some degree of conceptual overlap,
redundancies, and discrepancies.</p>

<h3 id="problem-statement">Problem Statement</h3>

<p>When integrating data and knowledge from heterogeneous sources that refer to the
same concepts using different identifiers, we get non-trivial duplications and
missing connections in our results.</p>

<p>For example, if we constructed a knowledge graph by combining the
<a href="https://ctdbase.org">Comparative Toxicogenomics Database (CTD)</a> and the
<a href="https://semantic.farm/mondo">Monarch Disease Ontology (MONDO)</a>, we would get
disconnected mechanisms describing how
<a href="https://ctdbase.org/detail.go?type=chem&amp;acc=C003402">sapropterin</a> is used to
treat <a href="https://semantic.farm/MONDO:0009861">phenylketonuria</a> because the CTD
uses the <a href="https://semantic.farm/mesh">Medical Subject Headings (MeSH)</a> to
describe genes/proteins and MONDO uses the
<a href="https://semantic.farm/hgnc">HUGO Gene Nomenclature Committee (HGNC)</a>.</p>

<pre><code class="language-mermaid">flowchart LR
    a["sapropterin (mesh:C003402)"] -- activates --&gt; b["Phenylalanine Hydroxylase (mesh:D010651)"]
    c["phenylalanine hydroxylase (HGNC:8582)"] -- decreases --&gt; d["phenylketonuria (MONDO:0009861)"]
    b -...-|missing connection, these should be collapsed together| c
</code></pre>

<p>As a consequence, these redundancies lead to inaccurate results, for example,
when making queries between drugs and diseases or when using machine learning
algorithms to make predictions for new edges.</p>

<h3 id="causes">Causes</h3>

<p>I roughly classify these redundancies into three bins (from left to right in the
figure): similar domain, hierarchically related domain, and non-specific to a
domain. Below, I’ll give some concrete examples from the life sciences to
illustrate.</p>

<p><img src="/img/mappings-automated-assembly/overlaps.svg" alt="" /></p>

<p>In chemistry, there are dozens of resources that assign identifiers to small
molecules. Many have been constructed with unique scope or purpose such as
MetaboLights for metabolites, SwissLipids for lipids, DrugBank for drugs. Some
have similar scope and purpose, but have been constructed in parallel due to
scientific modeling reasons, such as different disease ontologies modeling
diseases with different parts of the Basic Formal Ontology (BFO). Some have
similar scope and purpose, but have been constructed in parallel due to
non-scientific reasons, such as PubChem and ChEMBL for small molecules with
assay information. As an aside, building resources in an open and collaborative
manner can help reduce proliferation, with the (major) caveat that they don’t
satisfy funding bodies nor the requirements for career progression so easily.</p>

<p>In medicine and epidemiology, there are many resources describing diseases,
transmission, response, adverse outcomes, and other facets. Particularly during
the COVID-19 pandemic, many independent controlled vocabularies were constructed
to model information at various levels of specificity. The figure shows the
hierarchical relationships between the
<a href="https://semantic.farm/doid">Disease Ontology (DOID)</a>, the
<a href="https://semantic.farm/ido">Infectious Disease Ontology (IDO)</a>, the
<a href="https://semantic.farm/vido">Viral Infectious Disease Ontology (VIDO)</a>, the
<a href="https://semantic.farm/cido">Coronavirus Infectious Disease Ontology (CIDO)</a>,
and the
<a href="https://semantic.farm/idocovid19">COVID-19 Infectious Disease Ontology (IDOCOVID19)</a>.
When effectively reusing terms (as OBO Foundry Ontologies often do), this
doesn’t create an issue, but in practice, many resources do not reuse terms for
various reasons.</p>

<p>In the life sciences, there are several controlled vocabularies that cover a
large number of domains such as the
<a href="https://semantic.farm/mesh">Medical Subject Headings (MeSH)</a>,
<a href="https://semantic.farm/ncit">National Cancer Institute Thesaurus (NCIT)</a>, and
<a href="https://semantic.farm/umls">Unified Medical Language System (UMLS)</a>. While they
give good coverage across many domains, these resources are often neither
detailed, precise enough, nor curated as ontologies. Therefore, many controlled
vocabularies use terms from these resources as a base and curate further.
However, this causes redundancy, and in many cases, the group does not correctly
cross-reference back. The
<a href="https://semantic.farm/registry/omit">Ontology for MicroRNA Target (OMIT)</a> even
imported the entirety of MeSH, but didn’t make any cross-references back to the
source, creating even more redundancy.</p>

<p>If you were wondering why for each domain, we couldn’t just have a single
resource, then please have a look at
<a href="https://xkcd.com/927">https://xkcd.com/927</a> :) Though, some resources that have
been around for a long time basically have a monopoly. For example, nobody in
their right mind in 2026 would start their own protein database to compete with
<a href="https://uniprot.org">UniProt</a>.</p>

<h2 id="assembly">Assembly</h2>

<p>I want to highlight two groups for whom resolving redundancy has a high impact,
but not necessarily high visibility. The first group is data scientists who
consume knowledge graphs, for example, for graph machine learning. This group is
often unaware of how graphs were constructed (see: any graph machine learning
literature since 2013 that blindly uses FB15k and WN18).</p>

<p>The second group is curators, who want to use a combination of terminology
services like the <a href="https://www.ebi.ac.uk/ols4/">Ontology Lookup Service (OLS)</a>
and text mining tools to annotate the literature with controlled vocabulary
terms. Curators don’t want to (and shouldn’t have to) understand the landscape
of related controlled vocabularies for their domain and should just be
responsible with terminology services and text mining tools to find <em>any</em>
appropriate term for their curation.</p>

<p>The important point is that software should solve the problem of redundancy, and
it needs to do so by consuming semantic mappings that bridge the gap illustrated
above in the phenylketonuria example.</p>

<p><img src="/img/mappings-automated-assembly/overlaps-deal-with-it.svg" alt="" /></p>

<p>This leads to the main goal of the post, which is to describe two high-level
workflows that can resolve redundancies and discrepancies when integration data
and knowledge by using semantic mappings. This post isn’t about where semantic
mappings come from - see my other posts on SSSOM, JSKOS, and SeMRA for more
background on that.</p>

<h3 id="knowledge-graph-assembly">Knowledge Graph Assembly</h3>

<p>Resolving redundancies when constructing a knowledge graph means standardizing
the subjects, predicates, and objects in triples. For example, if we have
knowledge about ethanol from multiple sources and some identify it using the
ChEBI identifier <code class="language-plaintext highlighter-rouge">16236</code> while others identify it using the DrugBank identifier
<code class="language-plaintext highlighter-rouge">DB000898</code>, we will have a similar issue to the phenylketonuria approach. If we
have semantic mappings that denote <code class="language-plaintext highlighter-rouge">CHEBI:16236</code> and <code class="language-plaintext highlighter-rouge">drugbank:DB000898</code> are
equivalent, as well as the ruleset that ChEBI identifiers take precedent over
DrugBank identifiers, then we can map the triples from the resource that uses
DrugBank like described in the figure below:</p>

<p><img src="/img/mappings-automated-assembly/knowledge-assembly.svg" alt="" /></p>

<p>Let’s take the ChEBI Ontology and DrugBank pharmacological data as two examples
that both annotate chemical roles. Here are a few scenarios for a given DrugBank
entry (assuming they both use the same <code class="language-plaintext highlighter-rouge">rdfs:subClassOf</code> relationship):</p>

<ol>
  <li>There are no semantic mappings linking it to a ChEBI entry. In this case, the
subject doesn’t need to be mapped.</li>
  <li>There is a semantic mapping linking it to a ChEBI entry, but there’s no
semantic mapping linking the object to a ChEBI entry. For example, DrugBank
annotates ethanol as
<a href="https://go.drugbank.com/categories/DBCAT003935">Agents Causing Muscle Toxicity (drugbank.category:DBCAT003935)</a>.
Therefore, the subject is mapped but the object is retained.</li>
  <li>There is a semantic mapping linking it to a CheBI entry and a semantic
mapping linking the object to the ChEBI entry. For example, ChEBI annotates
ethanol as a
<a href="https://semantic.farm/CHEBI:60643">NMDA receptor antagonist (CHEBI:60643)</a>
and DrugBank annotates ethanol as a
<a href="https://go.drugbank.com/categories/DBCAT002723">NMDA Receptor Antagonists (drugbank.category:DBCAT002723)</a>.
In this case, the DrugBank triple is fully a duplicate of the ChEBI one.
However, it may have valuable metadata to keep.</li>
</ol>

<p>As another example, DrugBank annotates ethanol as a
<a href="https://go.drugbank.com/categories/DBCAT003232">Cytochrome P-450 CYP3A4 Inhibitors (drugbank.category:DBCAT003232)</a>.
There’s a corresponding term in ChEBI
<a href="https://www.ebi.ac.uk/chebi/search?query=CYP3A4%20Inhibitors">EC 1.14.13.97 (taurochenodeoxycholate 6α-hydroxylase) inhibitor, (CHEBI:86501)</a>,
but there isn’t a semantic mapping capturing this. This is a job for the
<a href="https://github.com/cthoyt/sssom-curator/">SSSOM Curator</a> software and a
semantic mapping repository like
<a href="https://github.com/biopragmatics/biomappings">Biomappings</a> to store it.</p>

<p>There are <em>many</em> examples of manually constructed workflows that do this
process. While these work (e.g., <a href="https://het.io">Hetionet</a> was one of the
best), they are brittle towards expansion to new datasets and mappings, and
often hard to keep up-to-date. My goal has been to implement a fully generic and
automated version of this workflow, which I did as part of the
<a href="https://github.com/biopragmatics/semra">Semantic Reasoner and Assembler</a>.
However, describing how it works on a technical level will be part of a future
post.</p>

<h3 id="lexicon-assembly">Lexicon Assembly</h3>

<p>Controlled vocabularies often contain labels and synonyms for their terms that
are useful when constructing lexical indexes (i.e., databases of labels and
synonyms) that can be fed into named entity recognition (NER) and named entity
normalization (NEN) workflows - crucial components of natural language
processing and text mining workflows that are commonly used by curators to
annotate the literature with relationships that eventually become part of
databases that are used to construct knowledge graphs.</p>

<p>However, much like knowledge, synonyms for the same concept might be spread over
multiple different resources. Therefore, semantic mappings can be used to group
multiple terms together and pool all of their synonyms, which both improves
recall and reduces the number of duplicate groundings that might be given for a
given part of text.</p>

<p><img src="/img/mappings-automated-assembly/synonyms-assembly.svg" alt="" /></p>

<p>I’ve already made a technical implementation of this workflow in the
<a href="https://github.com/biopragmatics/biolexica">Biolexica</a> project, but I’m working
towards generalizing and rebranding it for use outside the biomedical domain. I
previously posted a <a href="/2025/12/19/annotating-the-literature-demo.html">simple
demonstration</a> using
the underlying NER and NEN technology stack, but just using MeSH - a future post
will show how this works with a coherent lexica for diseases, genes, and other
entity types.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="SSSOM" /><category term="semantic mappings" /><category term="knowledge graphs" /><summary type="html"><![CDATA[Data and knowledge originating from heterogeneous sources often use heterogeneous controlled vocabularies and/or ontologies for annotating named entities. Semantic mappings are essential towards resolving these discrepancies and integrating in a coherent way. This post highlights how this looks in two scenarios: when constructing a knowledge graph for graph machine learning and when constructing a comprehensive lexica for natural language processing, text mining, and curation.]]></summary></entry><entry><title type="html">Mapping from SSSOM to JSKOS</title><link href="https://cthoyt.com/2026/01/15/sssom-to-jskos.html" rel="alternate" type="text/html" title="Mapping from SSSOM to JSKOS" /><published>2026-01-15T10:42:00+00:00</published><updated>2026-01-15T10:42:00+00:00</updated><id>https://cthoyt.com/2026/01/15/sssom-to-jskos</id><content type="html" xml:base="https://cthoyt.com/2026/01/15/sssom-to-jskos.html"><![CDATA[<p><a href="https://gbv.github.io/jskos/">JSKOS (JSON for Knowledge Organization Systems)</a>
is a JSON-based data model for representing terminologies, thesauri,
classifications, and other semantic artifacts. Like the
<a href="https://mapping-commons.github.io/sssom/">Simple Standard for Sharing Ontological Mappings (SSSOM)</a>,
it can also encode semantic mappings. This post is about developing and
implementing a crosswalk between them in the
<a href="https://github.com/cthoyt/sssom-pydantic/pull/26">sssom-pydantic</a> Python
package.</p>

<h2 id="background-on-jskos">Background on JSKOS</h2>

<p>At its core, JSKOS implements the Simple Knowledge Organization System (SKOS)
data model and extends it with a data model inspired by Wikidata with the
following types:</p>

<p><img src="https://gbv.github.io/jskos/types.svg" alt="" /></p>

<p>JSKOS enables representing semantic mappings two ways:</p>

<ol>
  <li>using the <code class="language-plaintext highlighter-rouge">narrower</code>, <code class="language-plaintext highlighter-rouge">broader</code>, and <code class="language-plaintext highlighter-rouge">related</code> slots in the
<a href="https://gbv.github.io/jskos/#concept">Concept</a> class that correspond to SKOS
relations <code class="language-plaintext highlighter-rouge">skos:narrowMatch</code>, <code class="language-plaintext highlighter-rouge">skos:broadMatch</code>, and <code class="language-plaintext highlighter-rouge">skos:relatedMatch</code></li>
  <li>using the <code class="language-plaintext highlighter-rouge">mappings</code> slot in the
<a href="https://gbv.github.io/jskos/#concept">Concept</a> class, which accepts a list
of instances of the more generic
<a href="https://gbv.github.io/jskos/#mapping">Mapping</a> class</li>
</ol>

<p>Here’s how JSKOS represents an exact match from the
<a href="https://github.com/biopragmatics/biomappings">Biomappings</a> community curated
mappings database between a
<a href="https://semantic.farm/mesh">Medical Subject Headings (MeSH)</a> term and
<a href="https://semantic.farm/chebi">Chemical Entities of Biological Interest (ChEBI) ontology</a>
term for the chemical <a href="https://en.wikipedia.org/wiki/Ammeline">ammeline</a>:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"license"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nl">"uri"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://spdx.org/licenses/CC0-1.0"</span><span class="w"> </span><span class="p">}],</span><span class="w">
  </span><span class="nl">"uri"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://w3id.org/biopragmatics/biomappings/sssom/biomappings.sssom.tsv"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"mappings"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"http://www.w3.org/2004/02/skos/core#exactMatch"</span><span class="p">],</span><span class="w">
      </span><span class="nl">"subject_bundle"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"member_set"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nl">"uri"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://id.nlm.nih.gov/mesh/C000089"</span><span class="w"> </span><span class="p">}]</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"object_bundle"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"member_set"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nl">"uri"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://purl.obolibrary.org/obo/CHEBI_28646"</span><span class="w"> </span><span class="p">}]</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"justification"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://w3id.org/semapv/vocab/ManualMappingCuration"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Notably, JSKOS is baked into the <a href="https://coli-conc.gbv.de/cocoda/">Cocoda</a>
mapping editor, which is being widely adopted in the humanities consortia of the
NFDI.</p>

<h2 id="interoperability-between-sssom-and-jskos">Interoperability between SSSOM and JSKOS</h2>

<p>Given the overlapping ability of the
<a href="https://mapping-commons.github.io/sssom/">Simple Standard for Sharing Ontological Mappings (SSSOM)</a>
and JSKOS to represent semantic mappings, the JSKOS and SSSOM teams developed a
<a href="https://github.com/gbv/jskos/issues/108">crosswalk</a> between JSKOS and SSSOM.
Along the way, the SSSOM and JSKOS data models evolved to incorporate good ideas
from the other, for example, the addition of a
<a href="https://github.com/mapping-commons/sssom/issues/359">mapping identifier</a> to
SSSOM records to allow for referencing the SSSOM mapping itself.</p>

<p>The crosswalk is not (yet) lossless, for example, JSKOS does not yet have a
mechanism to express
<a href="https://github.com/gbv/jskos/issues/152">information about lexical and other automated mappings</a>.
However, lossless conversion between data models isn’t always possible, nor is
it always necessary, considering the different domains for which JSKOS and SSSOM
were developed. That JSKOS was developed by researchers in the digital
humanities and SSSOM was developed by researchers in the life and natural
sciences can contextualize some of their discrepancies.</p>

<h2 id="technical-implementation">Technical Implementation</h2>

<p>The <a href="https://github.com/gbv/sssom-js">sssom-js</a> JavaScript package contains the
first SSSOM to JSKOS converter and has an
<a href="https://github.com/gbv/sssom-js/issues/5">open issue</a> for conversion back to
SSSOM (TSV). It was developed by the JSKOS team, meaning that I have high
confidence that the implemenation of the crosswalk is accurate.</p>

<p>While it can be invoked from the command line using <code class="language-plaintext highlighter-rouge">npx</code> like in
<code class="language-plaintext highlighter-rouge">npx sssom-js --from tsv --to jskos --output output.json input.sssom.tsv</code>, it
can also be explored in the first-party SSSOM Validation and Transformation
<a href="https://gbv.github.io/sssom-js/">website</a>.</p>

<p><img src="/img/sssom-js-validator.png" alt="" /></p>

<p>Originally, my plan was to implement SSSOM to JSKOS export in the
<a href="https://github.com/cthoyt/sssom-pydantic">sssom-pydantic</a> so it can be easily
incorporated into other SSSOM-aware applications like
<a href="https://github.com/cthoyt/sssom-curator/">SSSOM Curator</a> and the
<a href="https://github.com/biopragmatics/semra">Semantic Mapping Reasoner and Assembler</a>.</p>

<p>I started by implementing an object model for JSKOS in Python using Pydantic in
<a href="https://github.com/cthoyt/jskos">a dedicated package</a>. This actually turned out
to be very difficult to get to work in general because the JSKOS data model is
hierarchical and does not always contain fields that make it possible to
discriminate between which class a given arbitrary JSON object follows. This
makes it difficult to use Pydantic’s
<a href="https://docs.pydantic.dev/latest/concepts/unions/#nested-discriminated-unions">nested discriminated unions</a>
feature, so I had to implement a custom solution.</p>

<p>Ultimately, I scrapped the idea of re-implementing the crosswalk myself (for
now) and instead defer to the <code class="language-plaintext highlighter-rouge">sssom-js</code> implementation (while wrapping it in an
idiomatic Python API). Once <code class="language-plaintext highlighter-rouge">sssom-js</code> implements a TSV exporter, I will have a
high-quality oracle against which to test my implementation. These first steps
were implemented in
<a href="https://github.com/cthoyt/sssom-pydantic/pull/26">cthoyt/sssom-pydantic#26</a>.</p>

<p>Here’s what this looks like in Python:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">sssom_pydantic</span>
<span class="kn">from</span> <span class="nn">sssom_pydantic.contrib.jskos_export</span> <span class="kn">import</span> <span class="n">to_jskos</span>

<span class="n">url</span> <span class="o">=</span> <span class="s">"https://w3id.org/biopragmatics/biomappings/sssom/biomappings.sssom.tsv"</span>
<span class="n">mappings</span><span class="p">,</span> <span class="n">converter</span><span class="p">,</span> <span class="n">metadata</span> <span class="o">=</span> <span class="n">sssom_pydantic</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>

<span class="n">jskos_concept</span> <span class="o">=</span> <span class="n">to_jskos</span><span class="p">(</span><span class="n">mappings</span><span class="p">,</span> <span class="n">converter</span><span class="o">=</span><span class="n">converter</span><span class="p">,</span> <span class="n">metadata</span><span class="o">=</span><span class="n">metadata</span><span class="p">)</span>
</code></pre></div></div>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="SSSOM" /><category term="SKOS" /><category term="semantic mappings" /><category term="mappings" /><category term="interoperability" /><category term="JSKOS" /><summary type="html"><![CDATA[JSKOS (JSON for Knowledge Organization Systems) is a JSON-based data model for representing terminologies, thesauri, classifications, and other semantic artifacts. Like the Simple Standard for Sharing Ontological Mappings (SSSOM), it can also encode semantic mappings. This post is about developing and implementing a crosswalk between them in the sssom-pydantic Python package.]]></summary></entry><entry><title type="html">Mapping from SSSOM to Wikidata</title><link href="https://cthoyt.com/2026/01/08/sssom-to-wikidata.html" rel="alternate" type="text/html" title="Mapping from SSSOM to Wikidata" /><published>2026-01-08T15:47:00+00:00</published><updated>2026-01-08T15:47:00+00:00</updated><id>https://cthoyt.com/2026/01/08/sssom-to-wikidata</id><content type="html" xml:base="https://cthoyt.com/2026/01/08/sssom-to-wikidata.html"><![CDATA[<p>At the
<a href="https://nfdi4chem.de/event/4-workshop-ontologies4chem">4th Ontologies4Chem Workshop</a>
in Limburg an der Lahn, I proposed an initial crosswalk between the
<a href="https://mapping-commons.github.io/sssom">Simple Standard for Sharing Ontological Mappings (SSSOM)</a>
and the <a href="https://www.wikidata.org">Wikidata</a> semantic mapping data model. This
post describes the motivation for this proposal and the concrete implementation
I’ve developed in <a href="https://github.com/cthoyt/sssom-pydantic"><code class="language-plaintext highlighter-rouge">sssom-pydantic</code></a>.</p>

<p>This work is part of the NFDI’s
<a href="https://github.com/nfdi-de/section-metadata-wg-onto">Ontology Harmonization and Mapping Working Group</a>,
which is interested in enabling interoperability between SSSOM and related data
standards that encode semantic mappings.</p>

<p>The TL;DR for this post is that I implemented a mapping from SSSOM to Wikidata
in <code class="language-plaintext highlighter-rouge">sssom-pydantic</code> in
<a href="https://github.com/cthoyt/sssom-pydantic/pull/32">cthoyt/sssom-pydantic#32</a>.
One high-level entrypoint is the following function, which reads an SSSOM file
and prepares
<a href="https://www.wikidata.org/wiki/Help:QuickStatements">QuickStatements</a> which can
be reviewed in the web browser, then uploaded to Wikidata.</p>

<script src="https://gist.github.com/cthoyt/f38d37426a288989158a9804f74e731a.js"></script>

<p>This script can be run from Gist with
<code class="language-plaintext highlighter-rouge">uv run https://gist.github.com/cthoyt/f38d37426a288989158a9804f74e731a#file-sssom-wikidata-demo-py</code></p>

<h2 id="semantic-mappings-in-sssom">Semantic Mappings in SSSOM</h2>

<p>The
<a href="https://mapping-commons.github.io/sssom">Simple Standard for Sharing Ontological Mappings (SSSOM)</a>
is a community-driven data standard for semantic mappings, which are necessary
to support (semi-)automated data integration and knowledge integration, such as
in the construction of knowledge graphs.</p>

<p>While SSSOM primary a tabular data format that is best serialized in TSV, it
uses <a href="https://linkml.io">LinkML</a> to formalize the semantics of each field such
that SSSOM can be serialized to and read from OWL, RDF, and JSON-LD. Here’s a
brief example:</p>

<table>
  <thead>
    <tr>
      <th>subject_id</th>
      <th>subject_label</th>
      <th>predicate_id</th>
      <th>object_id</th>
      <th>object_label</th>
      <th>mapping_justification</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>wikidata:Q128700</td>
      <td>cell wall</td>
      <td>skos:exactMatch</td>
      <td>GO:0005618</td>
      <td>cell wall</td>
      <td>semapv:ManualMappingCuration</td>
    </tr>
    <tr>
      <td>wikidata:Q47512</td>
      <td>acetic acid</td>
      <td>skos:exactMatch</td>
      <td>CHEBI:15366</td>
      <td>acetic acid</td>
      <td>semapv:ManualMappingCuration</td>
    </tr>
  </tbody>
</table>

<h2 id="semantic-mappings-in-wikidata">Semantic Mappings in Wikidata</h2>

<p>Wikidata has two complementary formalisms for representing semantic mappings.
The first uses the
<a href="https://www.wikidata.org/wiki/Property:P2888">exact match (P2888)</a> property
with a URI as the object. For example,
<a href="https://www.wikidata.org/wiki/Q128700">cell wall (Q128700)</a> maps to the Gene
Ontology (GO) term for <a href="https://purl.obolibrary.org/obo/GO_0005618">cell wall</a>
by its URI <code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/GO_0005618</code>.</p>

<p><img src="/img/sssom-to-wikidata/cell-wall.png" alt="A screenshot of the exact match section of webpage for Wikidata's cell wall record" /></p>

<p>The second formalism uses semantic space-specific properties (e.g.
<a href="https://www.wikidata.org/wiki/Property:P683">P683</a> for ChEBI) with local unique
identifiers as the object. For example,
<a href="https://www.wikidata.org/wiki/Q47512">acetic acid (Q47512)</a> maps to the ChEBI
term for
<a href="https://www.ebi.ac.uk/chebi/searchId.do?chebiId=CHEBI:15366">acetic acid</a> using
the <a href="https://www.wikidata.org/wiki/Property:P683">P683</a> property for ChEBI and
local unique identifier for acetic acid (within ChEBI) <code class="language-plaintext highlighter-rouge">15366</code>.</p>

<p><img src="/img/sssom-to-wikidata/acetic-acid.png" alt="A screenshot of the ChEBI mapping section of webpage for Wikidata's acetic acid record" /></p>

<p>Wikidata has a data structure that enables annotating qualifiers onto triples.
Therefore, other parts of semantic mappings modeled in SSSOM can be ported:</p>

<ol>
  <li>Authors and reviewers can be mapped from ORCiD identifiers to Wikidata
identifiers, then encoded using the
<a href="https://www.wikidata.org/wiki/Property:P50">S50</a> and
<a href="https://www.wikidata.org/wiki/Property:P4032">S4032</a> properties,
respectively</li>
  <li>A SKOS-flavored mapping predicate (i.e., exact, narrow, broad, close,
related) can be encoded using the
<a href="https://www.wikidata.org/wiki/Property:P4390">S4390</a> property</li>
  <li>The publication date can be encoded using the
<a href="https://www.wikidata.org/wiki/Property:P577">S577</a> property</li>
  <li>The license can be mapped from text to a Wikidata identifier, then encoded
using the <a href="https://www.wikidata.org/wiki/Property:P275">S275</a> property</li>
</ol>

<p>Note that properties that normally start with a <code class="language-plaintext highlighter-rouge">P</code> when used in triples are
changed to start with an <code class="language-plaintext highlighter-rouge">S</code> when used as qualifiers. Other fields in SSSOM
could potentially be mapped to Wikidata later.</p>

<h3 id="finding-wikidata-properties-using-the-semantic-farm">Finding Wikidata Properties using the Semantic Farm</h3>

<p>The <a href="https://semantic.farm">Semantic Farm</a> (previously called the Bioregistry)
maintains mappings between prefixes that appear in compact URIs (CURIEs) and
their corresponding Wikidata properties. For example, the prefix
<a href="https://semantic.farm/chebi"><code class="language-plaintext highlighter-rouge">CHEBI</code></a> maps to the Wikidata property
<a href="https://www.wikidata.org/wiki/Property:P683">P683</a>.</p>

<p><img src="/img/sssom-to-wikidata/bioregistry.png" alt="" /></p>

<p>These mappings can be accessed in several ways:</p>

<ol>
  <li>via the Semantic Farm’s
<a href="https://raw.githubusercontent.com/biopragmatics/bioregistry/main/exports/sssom/bioregistry.sssom.tsv">SSSOM</a>
export. Note: this requires subsetting to mappings where Wikidata properties
are the object.</li>
  <li>via the Semantic Farm’s
<a href="https://semantic.farm/api/metaregistry/wikidata/mappings.json">live API</a>,</li>
  <li>
    <p>via the Bioregistry Python package (this will get renamed to match Semantic
Farm, eventually) using the following code:</p>

    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">bioregistry</span>

<span class="c1"># get bulk
</span><span class="n">prefix_to_property</span> <span class="o">=</span> <span class="n">bioregistry</span><span class="p">.</span><span class="n">get_registry_map</span><span class="p">(</span><span class="s">"wikidata"</span><span class="p">)</span>

<span class="c1"># get for a single resource
</span><span class="n">resource</span> <span class="o">=</span> <span class="n">bioregistry</span><span class="p">.</span><span class="n">get_resource</span><span class="p">(</span><span class="s">"chebi"</span><span class="p">)</span>
<span class="n">chebi_wikidata_property_id</span> <span class="o">=</span> <span class="n">resource</span><span class="p">.</span><span class="n">get_mapped_prefix</span><span class="p">(</span><span class="s">"wikidata"</span><span class="p">)</span>
</code></pre></div>    </div>
  </li>
</ol>

<h2 id="notable-implementation-details">Notable Implementation Details</h2>

<p>I’ve previously built two package which were key to making this work:</p>

<ol>
  <li><a href="https://github.com/cthoyt/wikidata-client"><code class="language-plaintext highlighter-rouge">wikidata-client</code></a>, which
interacts with the Wikidata SPARQL endpoint and has high-level wrappers
around lookup functionality. I’m also aware of
<a href="https://github.com/SuLab/WikidataIntegrator">WikidataIntegrator</a> - I’ve
contributed several improvements, but working with its codebase doesn’t spark
joy and the last time I tried to use it, it was fully broken due to some of
its dependencies not working on modern Python.</li>
  <li><a href="https://github.com/cthoyt/quickstatements-client"><code class="language-plaintext highlighter-rouge">quickstatements-client</code></a>,
which implements an object model for
<a href="https://www.wikidata.org/wiki/Help:QuickStatements">QuickStatements v2</a> and
an API client.</li>
</ol>

<p>Along the way to this PR, I made improvements to the wikidata-client in
<a href="https://github.com/cthoyt/wikidata-client/pull/2">cthoyt/wikidata-client#2</a> to
add high-level functionality for looking up multiple Wikidata records based on
values for a property (e.g., to support ORCID lookup in bulk).</p>

<p>All other changes were made in <code class="language-plaintext highlighter-rouge">sssom-pydantic</code> in
<a href="https://github.com/cthoyt/sssom-pydantic/pull/32">cthoyt/sssom-pydantic#32</a>.</p>

<p>The other key challenge was to avoid adding duplicate information to Wikidata -
unlike a simple triple store, we could accidentally end up with duplicate
statements. Therefore, the sssom-pydantic implementation looks up all existing
semantic mappings in Wikidata for entities appearing in an SSSOM file, then
filters appropriately to avoid uploading duplicate mappings to Wikidata.</p>

<h2 id="pulling-it-all-together">Pulling it All Together</h2>

<p>This new module in <code class="language-plaintext highlighter-rouge">sssom-pydantic</code> implements the following interactive
workflows:</p>

<ol>
  <li>Read an SSSOM file, convert mappings to Wikidata schema, then open a
QuickStatements tab in the web browser using
<code class="language-plaintext highlighter-rouge">read_and_open_quickstatements()</code></li>
  <li>Convert in-memory semantic mappings to the Wikidata schema, then open a
QuickStatements tab in the web browser using <code class="language-plaintext highlighter-rouge">open_quickstatements()</code></li>
</ol>

<p>Here’s what the QuickStatements web interface looks like after preparing some
demo mappings:</p>

<p><img src="/img/sssom-to-wikidata/quickstatements.png" alt="A screenshot of the QuickStatements queue" /></p>

<p>It also implements the following non-interactive workflows, which should be used
with caution since they write directly to Wikidata:</p>

<ol>
  <li>Read an SSSOM file, convert mappings to Wikidata schema, then post
non-interactively to Wikidata via QuickStatements using <code class="language-plaintext highlighter-rouge">read_and_post()</code></li>
  <li>Convert in-memory semantic mappings to the Wikidata schema, then post
non-interactively to Wikidata via QuickStatements using <code class="language-plaintext highlighter-rouge">post()</code></li>
</ol>

<hr />

<p>I’m a bit hesitant to start uploading SSSOM content to Wikidata in bulk, because
I don’t yet have a plan for how to maintain mappings that might change over time
in their upstream single source of truth, e.g., mappings curated in
<a href="https://github.com/biopragmatics/biomappings">Biomappings</a>. Otherwise, I think
this is a good proof of concept and would like to get feedback about additional
qualifiers that could be added, and if the ones I chose so far were the best.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="SSSOM" /><category term="Wikidata" /><category term="SKOS" /><category term="semantic mappings" /><category term="mappings" /><category term="interoperability" /><summary type="html"><![CDATA[At the 4th Ontologies4Chem Workshop in Limburg an der Lahn, I proposed an initial crosswalk between the Simple Standard for Sharing Ontological Mappings (SSSOM) and the Wikidata semantic mapping data model. This post describes the motivation for this proposal and the concrete implementation I’ve developed in sssom-pydantic.]]></summary></entry><entry><title type="html">Validating Prefix Maps in LinkML Schemas</title><link href="https://cthoyt.com/2026/01/06/bioregistry-linkml-validation.html" rel="alternate" type="text/html" title="Validating Prefix Maps in LinkML Schemas" /><published>2026-01-06T09:36:00+00:00</published><updated>2026-01-06T09:36:00+00:00</updated><id>https://cthoyt.com/2026/01/06/bioregistry-linkml-validation</id><content type="html" xml:base="https://cthoyt.com/2026/01/06/bioregistry-linkml-validation.html"><![CDATA[<p><a href="https://linkml.io">LinkML</a> enables defining data models and data schemas in
YAML informed by semantic web best practices. As such, each definition includes
a prefix map. Similarly to my previous posts on validating the prefix maps
appearing in <a href="/2025/09/04/bioregistry-turtle-validation.html">Turtle
files</a> and <a href="/2025/09/11/nfdi4culture-prefix-validation.html">in
unfamiliar SPARQL
endpoints</a>, this post
showcases describes a new extension to
<a href="https://github.com/biopragmatics/bioregistry">the Bioregistry</a> that validates
prefix maps in LinkML definitions.</p>

<p>Here’s an abridged excerpt of a LinkML
<a href="https://github.com/HendrikBorgelt/CatCore/blob/main/src/catcore/schema/catcore.yaml">definition</a>
borrowed from <a href="https://github.com/HendrikBorgelt/CatCore">CatCore</a>, a data model
under development by NFDI4Cat, the NFDI consortium interested in catalysis:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">id</span><span class="pi">:</span> <span class="s">https://w3id.org/nfdi4cat/catcore</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">catcore-metadata</span>
<span class="na">title</span><span class="pi">:</span> <span class="s">CatCore Metadata Reference Model</span>

<span class="na">prefixes</span><span class="pi">:</span>
  <span class="na">catcore</span><span class="pi">:</span> <span class="s">https://w3id.org/nfdi4cat/catcore/</span>
  <span class="na">voc4cat</span><span class="pi">:</span> <span class="s">https://w3id.org/nfdi4cat/voc4cat_</span>
  <span class="na">CHMO</span><span class="pi">:</span> <span class="s">http://purl.obolibrary.org/obo/CHMO_</span>
  <span class="na">OBI</span><span class="pi">:</span> <span class="s">http://purl.obolibrary.org/obo/OBI_</span>
  <span class="na">AFR</span><span class="pi">:</span> <span class="s">http://purl.allotrope.org/ontologies/result#AFR_</span>
  <span class="na">AFP</span><span class="pi">:</span> <span class="s">http://purl.allotrope.org/ontologies/process#AFP_</span>
  <span class="na">AFQ</span><span class="pi">:</span> <span class="s">http://purl.allotrope.org/ontologies/quality#AFQ_</span>
  <span class="na">NCIT</span><span class="pi">:</span> <span class="s">http://purl.obolibrary.org/obo/NCIT_</span>
  <span class="na">nmrCV</span><span class="pi">:</span> <span class="s2">"</span><span class="s">http://nmrML.org/nmrCV#NMR:"</span>
  <span class="na">linkml</span><span class="pi">:</span> <span class="s">https://w3id.org/linkml/</span>
  <span class="na">AFRL</span><span class="pi">:</span> <span class="s">http://purl.allotrope.org/ontologies/role#AFRL_</span>
  <span class="na">APOLLO_SV</span><span class="pi">:</span> <span class="s">http://purl.obolibrary.org/obo/APOLLO_SV_</span>
  <span class="na">SIO</span><span class="pi">:</span> <span class="s">http://semanticscience.org/resource/SIO_</span>

<span class="na">default_prefix</span><span class="pi">:</span> <span class="s">catcore</span>
</code></pre></div></div>

<p>In
<a href="https://github.com/biopragmatics/bioregistry/pull/1786">biopragmatics/bioregistry#1786</a>,
I implemented the <code class="language-plaintext highlighter-rouge">bioregistry validate linkml</code> command. It can be used to check
the prefix map in this file and give feedback on non-standard CURIE prefix
usage, unknown CURIE prefixes, etc. while giving suggestions for fixes, when
possible.</p>

<p>Running the command on the file that contains the example prefixes from above
gives the following output:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>bioregistry validate linkml <span class="nt">--tablefmt</span> github <span class="nt">--use-preferred</span> https://github.com/HendrikBorgelt/CatCore/raw/refs/heads/main/src/catcore/schema/catcore.yaml
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>prefix</th>
      <th>uri_prefix</th>
      <th>issue</th>
      <th>solution</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>catcore</td>
      <td>https://w3id.org/nfdi4cat/catcore/</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>AFR</td>
      <td>http://purl.allotrope.org/ontologies/result#AFR_</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>AFP</td>
      <td>http://purl.allotrope.org/ontologies/process#AFP_</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>AFQ</td>
      <td>http://purl.allotrope.org/ontologies/quality#AFQ_</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>nmrCV</td>
      <td>http://nmrML.org/nmrCV#NMR:</td>
      <td>non-standard CURIE prefix</td>
      <td>Switch to preferred prefix: NMR</td>
    </tr>
    <tr>
      <td>AFRL</td>
      <td>http://purl.allotrope.org/ontologies/role#AFRL_</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>SIO</td>
      <td>http://semanticscience.org/resource/SIO_</td>
      <td>non-standard CURIE prefix</td>
      <td>Switch to preferred prefix: sio</td>
    </tr>
  </tbody>
</table>

<p>Curation feedback is not absolute - it’s always possible that the Bioregistry is
missing key content. Luckily, it conforms to the
<a href="https://www.nature.com/articles/s41597-024-03406-w">open data, open code, open infrastructure (O3)</a>
guidelines, so it’s easy for anyone to perform a
<a href="https://doi.org/10.32388/KBX9VO">drive-by curation</a> to fix any minor issues.
The Bioregistry has public, well-defined
<a href="https://github.com/biopragmatics/bioregistry?tab=contributing-ov-file">curation guidelines</a>,
<a href="https://github.com/biopragmatics/bioregistry?tab=coc-ov-file">code of conduct</a>,
and
<a href="https://github.com/biopragmatics/bioregistry/blob/main/docs/GOVERNANCE.md">project governance</a>
to support making curation contributions. Alternatively, the
<a href="github.com/biopragmatics/bioregistry/issues">issue tracker</a> allows
non-technical users to post requests that the Bioregistry team can follow up on.</p>

<p>Based on the output above, I made improvements to the Bioregistry in
<a href="https://github.com/biopragmatics/bioregistry/pull/1788">biopragmatics/bioregistry#1788</a>
to add four new prefixes for the Allotrope semantic spaces and add <code class="language-plaintext highlighter-rouge">SIO</code>
(stylized with capital letters) as the “preferred prefix” for the
<a href="https://semantic.farm/sio">Semantic Science Integrated Ontology</a>.</p>

<p>Note that LinkML is developed by members of the OBO Community, and therefore,
its prefixes often skew towards OBO community preferences. Therefore, you might
want to use the <code class="language-plaintext highlighter-rouge">--use-preferred</code> flag if a lot of your prefixes are stylized in
uppercase or with mixed case.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="LinkML" /><category term="Bioregistry" /><category term="prefix maps" /><category term="CURIEs" /><category term="URIs" /><summary type="html"><![CDATA[LinkML enables defining data models and data schemas in YAML informed by semantic web best practices. As such, each definition includes a prefix map. Similarly to my previous posts on validating the prefix maps appearing in Turtle files and in unfamiliar SPARQL endpoints, this post showcases describes a new extension to the Bioregistry that validates prefix maps in LinkML definitions.]]></summary></entry><entry><title type="html">Books I Read in 2025</title><link href="https://cthoyt.com/2026/01/01/books-in-2025.html" rel="alternate" type="text/html" title="Books I Read in 2025" /><published>2026-01-01T21:22:00+00:00</published><updated>2026-01-01T21:22:00+00:00</updated><id>https://cthoyt.com/2026/01/01/books-in-2025</id><content type="html" xml:base="https://cthoyt.com/2026/01/01/books-in-2025.html"><![CDATA[<p>Here are the books I read in 2025. My goals for the year were to get some more
variety, and I think I managed that.</p>

<ol>
  <li>Turning Darkness Into Light (The Memoirs of Lady Trent, #6) by Marie Brennan</li>
  <li>Jade City (The Green Bone Saga #1) by Fonda Lee</li>
  <li>Jade War (The Green Bone Saga #2) by Fonda Lee</li>
  <li>Jade Legacy (The Green Bone Saga #3) by Fonda Lee</li>
  <li>A Court of Mist and Fury (ACOTAR, #2) by Sarah J. Maas</li>
  <li>Everyone You Hate is Going to Die by Daniel Sloss</li>
  <li>Intimacies by Katie Kitamura</li>
  <li>A Court of Wings and Ruin (ACOTAR, #3) by Sarah J. Maas</li>
  <li>The Raven Tower by Ann Leckie</li>
  <li>The Night Circus by Erin Morgenstern</li>
  <li>The Fall by Albert Camus</li>
  <li>The Lies of Locke Lamora (Gentleman Bastard, #1) by Scott Lynch</li>
  <li>The Will of the Many (Hierarchy, #1) by James Islington</li>
  <li>The Empress of Salt and Fortune by Nghi Vo</li>
  <li>Red Seas Under Red Skies (Gentleman Bastard, #2) by Scott Lynch</li>
  <li>Reckless by Cornelia Funke</li>
  <li>The Republic of Thieves (Gentleman Bastard, #3) by Scott Lynch</li>
  <li>Isles of the Emberdark by Brandon Sanderson</li>
  <li>The Priory of the Orange Tree by Samantha Shannon</li>
  <li>The River Has Roots by El-Mohtar Amal</li>
  <li>Marytr! by Kaveh Akbar</li>
  <li>A Man Called Ove by Fredrik Backman</li>
  <li>Open Throat by Henry Hoke</li>
  <li>The Midnight Library by Matt Haig</li>
  <li>The Tainted Cup (Shadow of the Leviathan, #1) by Robert Jackson Bennett</li>
  <li>The Catcher in the Rye by J.D. Salinger</li>
  <li>The Strength of the Few (Hierarchy, #2) by James Islington</li>
  <li>A Drop Of Corruption (Shadow of the Leviathan, #2) by Robert Jackson Bennett</li>
  <li>Piranesi by Susanna Clarke</li>
  <li>The Sun Also Rises by Ernest Hemingway</li>
</ol>

<p>All comments below are spoiler-free, except a minor note about the ending of
<em>Martyr!</em>.</p>

<p>Highlights:</p>

<ol>
  <li>The Night Circus was my favorite. It evoked a really special magical feeling</li>
  <li>The Green Bone Saga was an excellent trilogy, with perfect pacing,
progression, and knew the perfect spot to end.</li>
  <li>Title drops are very important. The ending of Open Throat was very cathartic.</li>
  <li>There was only one Brandon sandwich this year, but… SHARD GUNS</li>
</ol>

<p>Comments:</p>

<ul>
  <li>I was in a reading slump in the summer, so I also re-read Stormlight Archive.
Now knowing the end of Wind and Truth, it’s crazy to see the foreshadowing to
the end.</li>
  <li>I liked <em>The Tainted Cup</em> and <em>A Drop of Corruption</em> because Din and Ana are
very interesting characters and the world building is great. But, I’m not sure
if I’m sold on mystery, at least in books. I enjoy film and television
adaptations, though.</li>
  <li>A new bookstore opened in Fall in Beuel that is hosting
<a href="https://beuelerbuchladen.de/veranstaltungen/">silent reading nights</a> once per
month. I had a great time going there with friends, and we even instituted our
own silent reading nights (with snacks).</li>
</ul>

<p>Disappointments:</p>

<ol>
  <li><em>The Will of the Many</em> perfected the genres and archetypes that it pulls
from, but <em>The Strength of the Few</em> mostly missed the mark. Because of the
twist from the end of the first book, it had to do a lot of world-building
and create new plot and character arcs for the protagonist(s). I think it
didn’t match the tone promise of the first novel and ultimately felt like the
whole book was three side quests with new side characters in whom I wasn’t as
invested in as in the first book. <em>The Strength of the Few</em> had severe middle
child syndrome and felt bloated, but I think that it can be salvaged in the
next (and last?) installment.</li>
  <li>(minor spoilers) <em>Martyr!</em> told the story of a deeply troubled man who wanted
to write a book on what makes death meaningful and decide himself if he
wanted to continue living. While it had a positive ending, I think it
ultimately failed to deliver any (well-organized) revelations about what
makes a meaningful life, a meaningful death, or on martyrdom, either in-story
to the protagonist or on a meta level to me as the reader. I would instead
look to novels by Kurt Vonnegut for this kind of cleverness.</li>
  <li>I’m still motivated to read classics, but was pretty disappointed with
<em>Catcher in the Rye</em> and <em>The Sun Also Rises</em>. I didn’t feel like they had
much going in terms of character, plot, or theme that secures their
longevity. Obviously, it was an achievement to write a character as hatable
as Holden Caulfield, but neither book had clear tone/plot/theme promises nor
delivered on them.</li>
</ol>

<p>Won’t Finish:</p>

<ol>
  <li>The Name of the Rose by Umberto Eco. The world building, characters, and
writing annoyed me, and I was completely checked out by the time the plot
started. We read this as part of book club in preparation for a trip to
<a href="https://kloster-eberbach.de">Kloster Eberbach</a>, where the adaptation of the
book was filmed. However, 4/6 of our bookclub DNF’d this one, and we
instituted a 200-page maximum for future books. At least the Kloster was
beautiful and had good wine!</li>
  <li>Neuromancer by William Gibson. This book single-handedly created the
cyberpunk aesthetic. However, the story was weak and the characters were
uncomfortably outdated, so I put this one down. I imagine many writers had a
similar experience and thought they could do better. This is probably why
there are so many good stories with cyberpunk aesthetic!</li>
</ol>

<p>Reading goals for 2026: more variety and more reading in German!</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="books" /><summary type="html"><![CDATA[Here are the books I read in 2025. My goals for the year were to get some more variety, and I think I managed that.]]></summary></entry><entry><title type="html">Annotating the Literature with Named Entity Recognition</title><link href="https://cthoyt.com/2025/12/19/annotating-the-literature-demo.html" rel="alternate" type="text/html" title="Annotating the Literature with Named Entity Recognition" /><published>2025-12-19T09:01:00+00:00</published><updated>2025-12-19T09:01:00+00:00</updated><id>https://cthoyt.com/2025/12/19/annotating-the-literature-demo</id><content type="html" xml:base="https://cthoyt.com/2025/12/19/annotating-the-literature-demo.html"><![CDATA[<p>Annotating the literature with mentions of key concepts from a given domain is
often the first step towards extracting more substantial structured knowledge.
This can be challenging, as it typically encompasses acquiring and processing
the relevant literature and ontologies then installing and applying
difficult-to-use named entity recognition (NER) workflows. This post highlights
software components I’ve implemented to simplify this workflow. I demonstrate it
by annotating the biomedical literature available through
<a href="https://pubmed.ncbi.nlm.nih.gov/">PubMed</a> with
<a href="https://semantic.farm/mesh">Medical Subject Headings (MeSH)</a> terms, and also
comment on how this can be generalized to other natural sciences, engineering,
and humanities disciplines.</p>

<p>I’ve been building software for the last ten years that simplifies and
democratizes access to these resources. Here, I’m going to highlight three
components:</p>

<ol>
  <li><a href="https://pubmed.ncbi.nlm.nih.gov/"><strong>PubMed Downloader</strong></a> provides a wrapper
around PubMed’s API and around bulk download and processing of the source
data. While this resource only contains biomedical text, its place in the
workflow can be replaced with any other text source.</li>
  <li><a href="https://github.com/cthoyt/ssslm"><strong>SSSLM</strong></a> provides a wrapper around NER
methods such as <a href="https://github.com/gyorilab/gilda">Gilda</a> and
<a href="https://github.com/explosion/spaCy">spaCy</a>. SSSLM uses a pared-down version
of Gilda as its default NER tool because Gilda is fast, interpretable, and
easy to install (after removing some parts). SSSLM and the methods it wraps
are fully domain-agnostic.</li>
  <li><a href="https://github.com/biopragmatics/pyobo"><strong>PyOBO</strong></a> provides a wrapper around
fetching and processing ontologies, controlled vocabularies, databases, and
other resources that can be used as a dictionary. It also has a high-level
workflow,
<a href="https://pyobo.readthedocs.io/en/latest/api/pyobo.get_grounder.html"><code class="language-plaintext highlighter-rouge">pyobo.get_grounder()</code></a>
for getting content into <code class="language-plaintext highlighter-rouge">ssslm</code>. It’s built on the
<a href="https://semantic.farm">Semantic Farm</a> (previously called the Bioregistry) to
enable it to find and access ontologies across disciplines.</li>
</ol>

<h2 id="demonstration">Demonstration</h2>

<p>The following is a demonstration on how to get the abstracts of 5 articles from
PubMed, perform named entity recognition (NER) using Medical Subject Headings
(MeSH), output the results (below). Note that the following code can be run as a
script using <code class="language-plaintext highlighter-rouge">uv run</code>, as it makes explicit its dependencies as
<a href="https://peps.python.org/pep-0723/">PEP-723</a> inline metadata .</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># /// script
# requires-python = "&gt;=3.12"
# dependencies = [
#     "click&gt;=8.3.1",
#     "pubmed-downloader&gt;=0.0.12",
#     "pyobo[gilda-slim]&gt;=0.12.13",
#     "tabulate&gt;=0.9.0",
# ]
# ///
</span>
<span class="kn">import</span> <span class="nn">click</span>
<span class="kn">import</span> <span class="nn">pubmed_downloader</span>
<span class="kn">import</span> <span class="nn">pyobo</span>
<span class="kn">from</span> <span class="nn">tabulate</span> <span class="kn">import</span> <span class="n">tabulate</span>

<span class="c1"># get a grounder loaded up with a specific version of MeSH.
# if you don't specify a version, the latest will be used.
</span><span class="n">grounder</span><span class="p">:</span> <span class="n">ssslm</span><span class="p">.</span><span class="n">Grounder</span> <span class="o">=</span> <span class="n">pyobo</span><span class="p">.</span><span class="n">get_grounder</span><span class="p">(</span><span class="s">"mesh"</span><span class="p">,</span> <span class="n">versions</span><span class="o">=</span><span class="s">"2018"</span><span class="p">)</span>

<span class="c1"># get 5 PubMed identifiers about diabetes. note that the
# PubMed API has been horrifically slow lately, so please be patient
</span><span class="n">pubmed_ids</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">pubmed_downloader</span><span class="p">.</span><span class="n">search</span><span class="p">(</span><span class="s">"diabetes"</span><span class="p">,</span> <span class="n">backend</span><span class="o">=</span><span class="s">"api"</span><span class="p">,</span> <span class="n">retmax</span><span class="o">=</span><span class="mi">5</span><span class="p">)</span>
<span class="n">click</span><span class="p">.</span><span class="n">echo</span><span class="p">(</span><span class="sa">f</span><span class="s">"got </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">pubmed_ids</span><span class="p">)</span><span class="si">}</span><span class="s"> pubmed IDs"</span><span class="p">)</span>

<span class="k">for</span> <span class="n">article</span> <span class="ow">in</span> <span class="n">pubmed_downloader</span><span class="p">.</span><span class="n">get_articles</span><span class="p">(</span><span class="n">pubmed_ids</span><span class="p">,</span> <span class="n">error_strategy</span><span class="o">=</span><span class="s">"skip"</span><span class="p">,</span> <span class="n">progress</span><span class="o">=</span><span class="bp">True</span><span class="p">):</span>
    <span class="n">abstract</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">article</span><span class="p">.</span><span class="n">get_abstract</span><span class="p">()</span>

    <span class="c1"># get a list of annotations, which contain the offsets of the entity
</span>    <span class="c1"># and the grounding to a Bioregistry-standardized CURIE.
</span>    <span class="c1"># more generally, this can be applied to any string from any source
</span>    <span class="n">annotations</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="s">"ssslm.Annotation"</span><span class="p">]</span> <span class="o">=</span> <span class="n">grounder</span><span class="p">.</span><span class="n">annotate</span><span class="p">(</span><span class="n">abstract</span><span class="p">)</span>

    <span class="n">rows</span> <span class="o">=</span> <span class="p">[</span>
        <span class="p">(</span>
            <span class="n">annotation</span><span class="p">.</span><span class="n">start</span><span class="p">,</span>
            <span class="n">annotation</span><span class="p">.</span><span class="n">end</span><span class="p">,</span>
            <span class="sa">f</span><span class="s">"[</span><span class="si">{</span><span class="n">annotation</span><span class="p">.</span><span class="n">curie</span><span class="si">}</span><span class="s">](https://semantic.farm/</span><span class="si">{</span><span class="n">annotation</span><span class="p">.</span><span class="n">curie</span><span class="si">}</span><span class="s">)"</span><span class="p">,</span>
            <span class="n">annotation</span><span class="p">.</span><span class="n">name</span><span class="p">,</span>
            <span class="nb">round</span><span class="p">(</span><span class="n">annotation</span><span class="p">.</span><span class="n">score</span><span class="p">,</span> <span class="mi">3</span><span class="p">),</span>
        <span class="p">)</span>
        <span class="k">for</span> <span class="n">annotation</span> <span class="ow">in</span> <span class="n">annotations</span>
    <span class="p">]</span>
    <span class="n">headers</span> <span class="o">=</span> <span class="p">[</span><span class="s">"Start"</span><span class="p">,</span> <span class="s">"End"</span><span class="p">,</span> <span class="s">"CURIE"</span><span class="p">,</span> <span class="s">"Name"</span><span class="p">,</span> <span class="s">"Score"</span><span class="p">]</span>
    <span class="n">table</span> <span class="o">=</span> <span class="n">tabulate</span><span class="p">(</span><span class="n">rows</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">,</span> <span class="n">tablefmt</span><span class="o">=</span><span class="s">"github"</span><span class="p">)</span>

    <span class="n">click</span><span class="p">.</span><span class="n">echo</span><span class="p">(</span>
        <span class="sa">f</span><span class="s">"**</span><span class="si">{</span><span class="n">article</span><span class="p">.</span><span class="n">title</span><span class="p">.</span><span class="n">rstrip</span><span class="p">().</span><span class="n">rstrip</span><span class="p">(</span><span class="s">'.'</span><span class="p">)</span><span class="si">}</span><span class="s">** "</span>
        <span class="sa">f</span><span class="s">"([pubmed:</span><span class="si">{</span><span class="n">article</span><span class="p">.</span><span class="n">pubmed</span><span class="si">}</span><span class="s">](https://semantic.farm/pubmed:</span><span class="si">{</span><span class="n">article</span><span class="p">.</span><span class="n">pubmed</span><span class="si">}</span><span class="s">))"</span>
        <span class="sa">f</span><span class="s">"</span><span class="se">\n\n</span><span class="s">&gt; </span><span class="si">{</span><span class="n">abstract</span><span class="si">}</span><span class="se">\n\n</span><span class="si">{</span><span class="n">table</span><span class="si">}</span><span class="se">\n\n</span><span class="s">"</span>
    <span class="p">)</span>
</code></pre></div></div>

<h2 id="parting-thoughts">Parting Thoughts</h2>

<p>Normally I post parting thoughts at the bottom of each post, but since the
results take up a lot of space, I’ll put them here.</p>

<p>There are many directions to take these tools. The first might be to use a
subset of MeSH that’s most appropriate for the annotation task. For example, if
we just wanted to see diseases, then it only makes sense to use the MeSH Disease
branch. Similarly, there are many other ontologies, controlled vocabularies, and
databases in the diseases space such as MONDO, DOID, SNOMED-CT, and many more.
These can be incorporated into the grounder with
<code class="language-plaintext highlighter-rouge">pyobo.get_grounder(["mesh", "mondo", "doid", "snomedct])</code>, but will lead to
redundancy issues. I’ve previously published
<a href="https://github.com/biopragmatics/semra">SeMRA</a> where I addressed mapping
between equivalent entities, but am currently working on using these results to
assemble coherent and comprehensive lexica that can be easily reused by SSSLM in
the <a href="https://github.com/biopragmatics/biolexica">Biolexica project</a> (which will
also get renamed to be domain-agnostic).</p>

<p>Other domains can be directly used. For example, in the energy domain, the
<a href="https://semantic.farm/oeo">Open Energy Ontology</a> can be used with
<code class="language-plaintext highlighter-rouge">pyobo.get_grounder("oeo")</code>. In general, the
<a href="https://semantic.farm">Semantic Farm</a> can be used to find ontologies from other
domains. Within the <a href="https://www.nfdi.de/?lang=en">NFDI</a>, there are
<a href="https://semantic.farm/collection/">collections</a> for each NFDI consortia that
contain lists of relevant ontologies, controlled vocabularies, databases, and
other resources that mint identifiers.</p>

<p>I hope this was a helpful introduction! If you’ve got questions about these
workflows or want to see a demo on your favorite literature source/ontology/NER
method, post an issue to the relevant package’s issue tracker.</p>

<h2 id="results">Results</h2>

<p><strong>Investigation of intake pattern of SGLT2 inhibitors among shift workers with
diabetes: a crossover study</strong>
(<a href="https://semantic.farm/pubmed:41413602">pubmed:41413602</a>)</p>

<blockquote>
  <p>Shift workers experience regular changes in their waking hours due to
fluctuating work schedules. The timing of their medication intake differs
depending on whether they are working a day or night shift. Sodium-glucose
co-transporter 2 ( SGLT2) inhibitors are prescribed once a day and are often
taken before or after breakfast. However, studies on the optimal dosing times
for the effective treatment of shift workers are lacking. In this study, we
investigated whether the effects were different by the pattern of SGLT2
inhibitor intake for shift workers with diabetes. Seven shift workers with
diabetes who were taking an SGLT2 inhibitor were analyzed. All participants
took the medication upon waking for 14 days, followed by administration at a
fixed time for another 14 days. Glucose levels were measured over 14 days when
the drug was administered either upon waking or at a fixed time of day. The
time in range (TIR), which indicates the percentage of time during which the
glucose level is within the range of 70-180 mg/dL, was used as the main
evaluation index. The mean HbA1c of the participants was 7.1%. The TIR was
88.5% in the administration upon waking group and 84.9% in the administration
at a fixed time group. No significant difference in TIR values was observed
between the two administration groups. A TIR of 70% or higher is recommended
to prevent the onset of diabetic complications. Consistent intake of SGLT2
inhibitors, regardless of whether it is during the day or night shift, may
help stabilize blood glucose levels in shift workers throughout the day and
night, thereby preventing the development of complications.</p>
</blockquote>

<table>
  <thead>
    <tr>
      <th>Start</th>
      <th>End</th>
      <th>CURIE</th>
      <th>Name</th>
      <th>Score</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>6</td>
      <td>13</td>
      <td><a href="https://semantic.farm/mesh:D009274">mesh:D009274</a></td>
      <td>Occupational Groups</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>82</td>
      <td>96</td>
      <td><a href="https://semantic.farm/mesh:D010561">mesh:D010561</a></td>
      <td>Personnel Staffing and Scheduling</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>219</td>
      <td>233</td>
      <td><a href="https://semantic.farm/mesh:D027981">mesh:D027981</a></td>
      <td>Symporters</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>317</td>
      <td>326</td>
      <td><a href="https://semantic.farm/mesh:D062408">mesh:D062408</a></td>
      <td>Breakfast</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>410</td>
      <td>417</td>
      <td><a href="https://semantic.farm/mesh:D009274">mesh:D009274</a></td>
      <td>Occupational Groups</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>515</td>
      <td>530</td>
      <td><a href="https://semantic.farm/mesh:D000077203">mesh:D000077203</a></td>
      <td>Sodium-Glucose Transporter 2 Inhibitors</td>
      <td>0.549</td>
    </tr>
    <tr>
      <td>548</td>
      <td>555</td>
      <td><a href="https://semantic.farm/mesh:D009274">mesh:D009274</a></td>
      <td>Occupational Groups</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>583</td>
      <td>590</td>
      <td><a href="https://semantic.farm/mesh:D009274">mesh:D009274</a></td>
      <td>Occupational Groups</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>624</td>
      <td>639</td>
      <td><a href="https://semantic.farm/mesh:D000077203">mesh:D000077203</a></td>
      <td>Sodium-Glucose Transporter 2 Inhibitors</td>
      <td>0.549</td>
    </tr>
    <tr>
      <td>729</td>
      <td>743</td>
      <td><a href="https://semantic.farm/mesh:D009934">mesh:D009934</a></td>
      <td>Organization and Administration</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>781</td>
      <td>788</td>
      <td><a href="https://semantic.farm/mesh:D005947">mesh:D005947</a></td>
      <td>Glucose</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>832</td>
      <td>836</td>
      <td><a href="https://semantic.farm/mesh:D004364">mesh:D004364</a></td>
      <td>Pharmaceutical Preparations</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>981</td>
      <td>988</td>
      <td><a href="https://semantic.farm/mesh:D005947">mesh:D005947</a></td>
      <td>Glucose</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1064</td>
      <td>1069</td>
      <td><a href="https://semantic.farm/mesh:D020481">mesh:D020481</a></td>
      <td>Index</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1141</td>
      <td>1155</td>
      <td><a href="https://semantic.farm/mesh:D009934">mesh:D009934</a></td>
      <td>Organization and Administration</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>1191</td>
      <td>1205</td>
      <td><a href="https://semantic.farm/mesh:D009934">mesh:D009934</a></td>
      <td>Organization and Administration</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>1298</td>
      <td>1312</td>
      <td><a href="https://semantic.farm/mesh:D009934">mesh:D009934</a></td>
      <td>Organization and Administration</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>1383</td>
      <td>1405</td>
      <td><a href="https://semantic.farm/mesh:D048909">mesh:D048909</a></td>
      <td>Diabetes Complications</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>1428</td>
      <td>1444</td>
      <td><a href="https://semantic.farm/mesh:D000077203">mesh:D000077203</a></td>
      <td>Sodium-Glucose Transporter 2 Inhibitors</td>
      <td>0.549</td>
    </tr>
    <tr>
      <td>1524</td>
      <td>1537</td>
      <td><a href="https://semantic.farm/mesh:D001786">mesh:D001786</a></td>
      <td>Blood Glucose</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1554</td>
      <td>1561</td>
      <td><a href="https://semantic.farm/mesh:D009274">mesh:D009274</a></td>
      <td>Occupational Groups</td>
      <td>0.54</td>
    </tr>
  </tbody>
</table>

<p><strong>Men’s health needs assessment in the Toledo District of Southern Belize</strong>
(<a href="https://semantic.farm/pubmed:41413521">pubmed:41413521</a>)</p>

<blockquote>
  <p>Belize is a small country in Central America with a growing burden of
non-communicable disease (NCD), including hypertension and diabetes. Toledo
District is the southernmost and poorest district in the country. Reliable
national level health data for Belize is readily available, but the data is
rarely disaggregated by sex or district. Reducing the burden of NCDs is a high
priority for the Ministry of Health and Wellness. Belize’s progress to date on
Sustainable Development Goal (SDG) 3 (Good Health and Wellbeing) has been
modest with many indicators stagnating or progress increasing at less than 50%
of the required rate. SDG 3 describes the need to reduce the risks of NCDs and
to strengthen the capacities of the healthcare workforce. The objective was to
perform a men’s health needs assessment to identify and prioritize men’s
health needs in the Toledo District. This was a mixed methods study.
Qualitative data were collected from semi-structured interviews. Interviews
were recorded, transcribed, and analyzed using Thematic Analysis. Quantitative
data included epidemiological data from national vital statistics or disease
registries and other public sources. Data were collected between January and
June 2017. Belizean men have among the highest risk for cardiac or diabetes
related illness or death in the Americas. Diabetes and hypertension are
responsible for 4.49% and 1.23% of Disability Adjusted Life Years in men
respectively and are increasing by 2.51% annually. Fifty-seven interviews (55
individuals and two groups) from nine villages were carried out. Four themes
emerged from the qualitative data. Men in Toledo: • have poor health literacy;
• have reasonable access to health resources, but do not use them; • inability
to clearly articulate health priorities; • do not process risk well. Men in
Toledo suffer from a high prevalence of NCDs including hypertension and
diabetes and understand health and risks poorly. This may contribute to
Belize’s struggle to achieve the goals of SDG 3.4.1. Strengthening the
healthcare workforce by improved training of community health workers (CHWs)
and providing health education to men in Toledo is required to address these
concerns.</p>
</blockquote>

<table>
  <thead>
    <tr>
      <th>Start</th>
      <th>End</th>
      <th>CURIE</th>
      <th>Name</th>
      <th>Score</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>6</td>
      <td><a href="https://semantic.farm/mesh:D001531">mesh:D001531</a></td>
      <td>Belize</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>29</td>
      <td>44</td>
      <td><a href="https://semantic.farm/mesh:D002489">mesh:D002489</a></td>
      <td>Central America</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>70</td>
      <td>94</td>
      <td><a href="https://semantic.farm/mesh:D000073296">mesh:D000073296</a></td>
      <td>Noncommunicable Diseases</td>
      <td>0.549</td>
    </tr>
    <tr>
      <td>112</td>
      <td>124</td>
      <td><a href="https://semantic.farm/mesh:D006973">mesh:D006973</a></td>
      <td>Hypertension</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>236</td>
      <td>242</td>
      <td><a href="https://semantic.farm/mesh:D006262">mesh:D006262</a></td>
      <td>Health</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>252</td>
      <td>258</td>
      <td><a href="https://semantic.farm/mesh:D001531">mesh:D001531</a></td>
      <td>Belize</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>321</td>
      <td>324</td>
      <td><a href="https://semantic.farm/mesh:D012723">mesh:D012723</a></td>
      <td>Sex</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>405</td>
      <td>411</td>
      <td><a href="https://semantic.farm/mesh:D006262">mesh:D006262</a></td>
      <td>Health</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>426</td>
      <td>432</td>
      <td><a href="https://semantic.farm/mesh:D001531">mesh:D001531</a></td>
      <td>Belize</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>455</td>
      <td>483</td>
      <td><a href="https://semantic.farm/mesh:D000076502">mesh:D000076502</a></td>
      <td>Sustainable Development</td>
      <td>0.556</td>
    </tr>
    <tr>
      <td>498</td>
      <td>504</td>
      <td><a href="https://semantic.farm/mesh:D006262">mesh:D006262</a></td>
      <td>Health</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>546</td>
      <td>556</td>
      <td><a href="https://semantic.farm/mesh:D007202">mesh:D007202</a></td>
      <td>Indicators and Reagents</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>669</td>
      <td>674</td>
      <td><a href="https://semantic.farm/mesh:D012306">mesh:D012306</a></td>
      <td>Risk</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>723</td>
      <td>733</td>
      <td><a href="https://semantic.farm/mesh:D003695">mesh:D003695</a></td>
      <td>Delivery of Health Care</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>734</td>
      <td>743</td>
      <td><a href="https://semantic.farm/mesh:D000078329">mesh:D000078329</a></td>
      <td>Workforce</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>776</td>
      <td>788</td>
      <td><a href="https://semantic.farm/mesh:D054526">mesh:D054526</a></td>
      <td>Men’s Health</td>
      <td>0.725</td>
    </tr>
    <tr>
      <td>789</td>
      <td>805</td>
      <td><a href="https://semantic.farm/mesh:D020380">mesh:D020380</a></td>
      <td>Needs Assessment</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>833</td>
      <td>845</td>
      <td><a href="https://semantic.farm/mesh:D054526">mesh:D054526</a></td>
      <td>Men’s Health</td>
      <td>0.725</td>
    </tr>
    <tr>
      <td>846</td>
      <td>851</td>
      <td><a href="https://semantic.farm/mesh:D006301">mesh:D006301</a></td>
      <td>Health Services Needs and Demand</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>893</td>
      <td>900</td>
      <td><a href="https://semantic.farm/mesh:D008722">mesh:D008722</a></td>
      <td>Methods</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1112</td>
      <td>1128</td>
      <td><a href="https://semantic.farm/mesh:D014798">mesh:D014798</a></td>
      <td>Vital Statistics</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1132</td>
      <td>1139</td>
      <td><a href="https://semantic.farm/mesh:D004194">mesh:D004194</a></td>
      <td>Disease</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1140</td>
      <td>1150</td>
      <td><a href="https://semantic.farm/mesh:D012042">mesh:D012042</a></td>
      <td>Registries</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1237</td>
      <td>1240</td>
      <td><a href="https://semantic.farm/mesh:D008571">mesh:D008571</a></td>
      <td>Men</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1264</td>
      <td>1268</td>
      <td><a href="https://semantic.farm/mesh:D012306">mesh:D012306</a></td>
      <td>Risk</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1312</td>
      <td>1317</td>
      <td><a href="https://semantic.farm/mesh:D003643">mesh:D003643</a></td>
      <td>Death</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1325</td>
      <td>1333</td>
      <td><a href="https://semantic.farm/mesh:D000569">mesh:D000569</a></td>
      <td>Americas</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>1348</td>
      <td>1360</td>
      <td><a href="https://semantic.farm/mesh:D006973">mesh:D006973</a></td>
      <td>Hypertension</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1400</td>
      <td>1430</td>
      <td><a href="https://semantic.farm/mesh:D000087509">mesh:D000087509</a></td>
      <td>Disability-Adjusted Life Years</td>
      <td>0.556</td>
    </tr>
    <tr>
      <td>1434</td>
      <td>1437</td>
      <td><a href="https://semantic.farm/mesh:D008571">mesh:D008571</a></td>
      <td>Men</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1628</td>
      <td>1631</td>
      <td><a href="https://semantic.farm/mesh:D008571">mesh:D008571</a></td>
      <td>Men</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>1655</td>
      <td>1670</td>
      <td><a href="https://semantic.farm/mesh:D057220">mesh:D057220</a></td>
      <td>Health Literacy</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1700</td>
      <td>1716</td>
      <td><a href="https://semantic.farm/mesh:D006295">mesh:D006295</a></td>
      <td>Health Resources</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1773</td>
      <td>1790</td>
      <td><a href="https://semantic.farm/mesh:D006292">mesh:D006292</a></td>
      <td>Health Priorities</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1809</td>
      <td>1813</td>
      <td><a href="https://semantic.farm/mesh:D012306">mesh:D012306</a></td>
      <td>Risk</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1820</td>
      <td>1823</td>
      <td><a href="https://semantic.farm/mesh:D008571">mesh:D008571</a></td>
      <td>Men</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>1853</td>
      <td>1863</td>
      <td><a href="https://semantic.farm/mesh:D015995">mesh:D015995</a></td>
      <td>Prevalence</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1882</td>
      <td>1894</td>
      <td><a href="https://semantic.farm/mesh:D006973">mesh:D006973</a></td>
      <td>Hypertension</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1923</td>
      <td>1929</td>
      <td><a href="https://semantic.farm/mesh:D006262">mesh:D006262</a></td>
      <td>Health</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1934</td>
      <td>1939</td>
      <td><a href="https://semantic.farm/mesh:D012306">mesh:D012306</a></td>
      <td>Risk</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1971</td>
      <td>1977</td>
      <td><a href="https://semantic.farm/mesh:D001531">mesh:D001531</a></td>
      <td>Belize</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>2004</td>
      <td>2009</td>
      <td><a href="https://semantic.farm/mesh:D006040">mesh:D006040</a></td>
      <td>Goals</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>2042</td>
      <td>2052</td>
      <td><a href="https://semantic.farm/mesh:D003695">mesh:D003695</a></td>
      <td>Delivery of Health Care</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>2053</td>
      <td>2062</td>
      <td><a href="https://semantic.farm/mesh:D000078329">mesh:D000078329</a></td>
      <td>Workforce</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>2087</td>
      <td>2111</td>
      <td><a href="https://semantic.farm/mesh:D003150">mesh:D003150</a></td>
      <td>Community Health Workers</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>2133</td>
      <td>2149</td>
      <td><a href="https://semantic.farm/mesh:D006266">mesh:D006266</a></td>
      <td>Health Education</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>2153</td>
      <td>2156</td>
      <td><a href="https://semantic.farm/mesh:D008571">mesh:D008571</a></td>
      <td>Men</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>2182</td>
      <td>2189</td>
      <td><a href="https://semantic.farm/mesh:D019484">mesh:D019484</a></td>
      <td>Address</td>
      <td>0.762</td>
    </tr>
  </tbody>
</table>

<p><strong>Risk factors of ventilator-associated pneumonia in patients with acute
exacerbation of chronic obstructive pulmonary disease: a meta-analysis and
systematic review</strong> (<a href="https://semantic.farm/pubmed:41413500">pubmed:41413500</a>)</p>

<blockquote>
  <p>This meta-analysis aimed to identify risk factors for ventilator-associated
pneumonia (VAP) in patients with Acute exacerbations of Chronic obstructive
pulmonary disease (AECOPD). We systematically searched PubMed, Web of Science,
CINAHL, Cochrane Library, Embase, CNKI, and other databases for studies
investigating risk factors for VAP in patients experiencing AECOPD. The search
encompassed records from database inception up to July 2, 2025. The quality of
the studies was assessed using the Newcastle-Ottawa Scale. Meta-analysis was
performed using Stata 18.0. A total of 16 articles were included, encompassing
3,664 subjects and 16 risk factors. Meta-analysis results showed that, Age
(OR: 2.49, 95%CI : 1.49, 4.17; P&lt;0.001), Smoking history (OR: 2.70, 95%CI :
1.65, 4.44; P&lt;0.001), Acute physiology and chronic health evaluation composite
score (APACHE Ⅱ) score (OR: 3.03, 95%CI : 1.98, 4.65; P&lt;0.001), Sequential
organ failure assessment (SOFA) score (OR: 2.75, 95%CI : 1.90, 3.99; P&lt;0.001),
Diabetes (OR: 2.11, 95%CI : 1.38, 3.24; P = 0.001), Underlying Diseases (OR:
3.42, 95%CI : 1.85, 6.32; P&lt;0.001), Duration of mechanical ventilation (OR:
4.53, 95%CI : 2.68, 7.65; P&lt;0.001), Tracheal intubation (OR: 4.21, 95%CI :
1.85, 9.57; P = 0.001), Indwelling gastric tube ( OR: 3.31, 95%CI : 1.38,
7.95; P = 0.008), Total parenteral nutrition (OR: 1.86, 95%CI : 1.29, 2.70; P
= 0.001), Combined antibiotics (OR: 2.79, 95%CI : 1.32, 5.93; P = 0.007),
Tracheotomy (OR: 2.92, 95%CI : 2.04, 4.17; P&lt;0.001), History of mechanical
ventilation within one year (OR: 2.92, 95%CI : 2.04, 4.17; P = 0.005), Use
acid suppressants (OR: 2.10, 95%CI : 1.49, 2.97; P&lt;0.001) were associated with
the development of VAP in AECOPD patients. This study identified 14 risk
factors associated with the risk of VAP in AECOPD patients. This finding is
helpful for early identification of high-risk patients, which is of great
value for reducing mortality and improving the clinical prognosis of patients
with mechanical ventilation.</p>
</blockquote>

<table>
  <thead>
    <tr>
      <th>Start</th>
      <th>End</th>
      <th>CURIE</th>
      <th>Name</th>
      <th>Score</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>5</td>
      <td>18</td>
      <td><a href="https://semantic.farm/mesh:D017418">mesh:D017418</a></td>
      <td>Meta-Analysis</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>37</td>
      <td>49</td>
      <td><a href="https://semantic.farm/mesh:D012307">mesh:D012307</a></td>
      <td>Risk Factors</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>54</td>
      <td>85</td>
      <td><a href="https://semantic.farm/mesh:D053717">mesh:D053717</a></td>
      <td>Pneumonia, Ventilator-Associated</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>132</td>
      <td>169</td>
      <td><a href="https://semantic.farm/mesh:D029424">mesh:D029424</a></td>
      <td>Pulmonary Disease, Chronic Obstructive</td>
      <td>0.549</td>
    </tr>
    <tr>
      <td>207</td>
      <td>213</td>
      <td><a href="https://semantic.farm/mesh:D039781">mesh:D039781</a></td>
      <td>PubMed</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>222</td>
      <td>229</td>
      <td><a href="https://semantic.farm/mesh:D012586">mesh:D012586</a></td>
      <td>Science</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>248</td>
      <td>255</td>
      <td><a href="https://semantic.farm/mesh:D007990">mesh:D007990</a></td>
      <td>Libraries</td>
      <td>0.556</td>
    </tr>
    <tr>
      <td>317</td>
      <td>329</td>
      <td><a href="https://semantic.farm/mesh:D012307">mesh:D012307</a></td>
      <td>Risk Factors</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>394</td>
      <td>401</td>
      <td><a href="https://semantic.farm/mesh:D011996">mesh:D011996</a></td>
      <td>Records</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>407</td>
      <td>415</td>
      <td><a href="https://semantic.farm/mesh:D019991">mesh:D019991</a></td>
      <td>Database</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>520</td>
      <td>533</td>
      <td><a href="https://semantic.farm/mesh:D017418">mesh:D017418</a></td>
      <td>Meta-Analysis</td>
      <td>0.772</td>
    </tr>
    <tr>
      <td>639</td>
      <td>651</td>
      <td><a href="https://semantic.farm/mesh:D012307">mesh:D012307</a></td>
      <td>Risk Factors</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>653</td>
      <td>666</td>
      <td><a href="https://semantic.farm/mesh:D017418">mesh:D017418</a></td>
      <td>Meta-Analysis</td>
      <td>0.772</td>
    </tr>
    <tr>
      <td>735</td>
      <td>742</td>
      <td><a href="https://semantic.farm/mesh:D012907">mesh:D012907</a></td>
      <td>Smoking</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>743</td>
      <td>750</td>
      <td><a href="https://semantic.farm/mesh:D006664">mesh:D006664</a></td>
      <td>History</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>794</td>
      <td>840</td>
      <td><a href="https://semantic.farm/mesh:D018806">mesh:D018806</a></td>
      <td>APACHE</td>
      <td>0.549</td>
    </tr>
    <tr>
      <td>858</td>
      <td>866</td>
      <td><a href="https://semantic.farm/mesh:D018806">mesh:D018806</a></td>
      <td>APACHE</td>
      <td>0.53</td>
    </tr>
    <tr>
      <td>1072</td>
      <td>1080</td>
      <td><a href="https://semantic.farm/mesh:D004194">mesh:D004194</a></td>
      <td>Disease</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>1072</td>
      <td>1080</td>
      <td><a href="https://semantic.farm/obo:mesh#C">obo:mesh#C</a></td>
      <td>Diseases</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>1136</td>
      <td>1158</td>
      <td><a href="https://semantic.farm/mesh:D012121">mesh:D012121</a></td>
      <td>Respiration, Artificial</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>1211</td>
      <td>1221</td>
      <td><a href="https://semantic.farm/mesh:D007440">mesh:D007440</a></td>
      <td>Intubation</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1332</td>
      <td>1358</td>
      <td><a href="https://semantic.farm/mesh:D010289">mesh:D010289</a></td>
      <td>Parenteral Nutrition, Total</td>
      <td>0.549</td>
    </tr>
    <tr>
      <td>1411</td>
      <td>1422</td>
      <td><a href="https://semantic.farm/mesh:D000900">mesh:D000900</a></td>
      <td>Anti-Bacterial Agents</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>1466</td>
      <td>1477</td>
      <td><a href="https://semantic.farm/mesh:D014140">mesh:D014140</a></td>
      <td>Tracheotomy</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>1521</td>
      <td>1528</td>
      <td><a href="https://semantic.farm/mesh:D006664">mesh:D006664</a></td>
      <td>History</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>1532</td>
      <td>1554</td>
      <td><a href="https://semantic.farm/mesh:D012121">mesh:D012121</a></td>
      <td>Respiration, Artificial</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>1767</td>
      <td>1779</td>
      <td><a href="https://semantic.farm/mesh:D012307">mesh:D012307</a></td>
      <td>Risk Factors</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1800</td>
      <td>1804</td>
      <td><a href="https://semantic.farm/mesh:D012306">mesh:D012306</a></td>
      <td>Risk</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1941</td>
      <td>1950</td>
      <td><a href="https://semantic.farm/mesh:D009026">mesh:D009026</a></td>
      <td>Mortality</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1978</td>
      <td>1987</td>
      <td><a href="https://semantic.farm/mesh:D011379">mesh:D011379</a></td>
      <td>Prognosis</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>2005</td>
      <td>2027</td>
      <td><a href="https://semantic.farm/mesh:D012121">mesh:D012121</a></td>
      <td>Respiration, Artificial</td>
      <td>0.54</td>
    </tr>
  </tbody>
</table>

<p><strong>Randomized trial assessing transverse supraumbilical incisions for cesarean
sections in morbid obese women with pannus</strong>
(<a href="https://semantic.farm/pubmed:41413498">pubmed:41413498</a>)</p>

<blockquote>
  <p>BACKGROUND AND OBJECTIVE: The high prevalence of Morbidly obese Egyptian
patients presents surgical problems for cesarean sections (CS), including a
higher risk of wound infections. This study examines the impact of a
transverse supraumbilical (TSU) incision in these patients. We conducted a
randomized controlled trial on 72 morbidly obese patients (BMI &gt;40 kg/m²)
scheduled for cesarean section at Ain Shams University Hospital from March
2016 to August 2018. Participants were divided into Group A (36 patients) with
a transverse supraumbilical (TSU) incision and Group B (36 patients) with a
conventional Pfannenstiel incision. The primary outcome measured was the
incidence of wound infection, while secondary outcomes included operative
time, postoperative pain, hospital stay, blood loss, postoperative mobility,
and intestinal motility. The results indicated no significant differences
between the groups regarding age, BMI, parity, diabetes mellitus, and history
of previous cesarean sections. The incidence of surgical site infection was
significantly lower in the transverse supraumbilical group (11.1%, 4/36)
compared to the Pfannenstiel group (58.3%, 21/36), with an absolute risk
reduction of 47.2% (95% CI: 27.8% to 66.6%). Other parameters like operative
time, hematocrit drop, pain score, hospital stay, and intestinal motility
showed no significant differences between the groups (P&gt;0.05). Supraumbilical
transverse incisions are a safe, effective alternative to Pfannenstiel
incisions in morbidly obese women, with better wound infection rates and
easier access. Further research is needed to confirm the benefits and to
assess patient satisfaction. This study was registered prospectively in
clinicaltrials.gov (NCT02692729) on 1.3.2016.</p>
</blockquote>

<table>
  <thead>
    <tr>
      <th>Start</th>
      <th>End</th>
      <th>CURIE</th>
      <th>Name</th>
      <th>Score</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>35</td>
      <td>45</td>
      <td><a href="https://semantic.farm/mesh:D015995">mesh:D015995</a></td>
      <td>Prevalence</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>113</td>
      <td>130</td>
      <td><a href="https://semantic.farm/mesh:D002585">mesh:D002585</a></td>
      <td>Cesarean Section</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>156</td>
      <td>160</td>
      <td><a href="https://semantic.farm/mesh:D012306">mesh:D012306</a></td>
      <td>Risk</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>164</td>
      <td>180</td>
      <td><a href="https://semantic.farm/mesh:D014946">mesh:D014946</a></td>
      <td>Wound Infection</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>293</td>
      <td>320</td>
      <td><a href="https://semantic.farm/mesh:D016449">mesh:D016449</a></td>
      <td>Randomized Controlled Trial</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>381</td>
      <td>397</td>
      <td><a href="https://semantic.farm/mesh:D002585">mesh:D002585</a></td>
      <td>Cesarean Section</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>411</td>
      <td>421</td>
      <td><a href="https://semantic.farm/mesh:D014495">mesh:D014495</a></td>
      <td>Universities</td>
      <td>0.556</td>
    </tr>
    <tr>
      <td>422</td>
      <td>430</td>
      <td><a href="https://semantic.farm/mesh:D006761">mesh:D006761</a></td>
      <td>Hospitals</td>
      <td>0.556</td>
    </tr>
    <tr>
      <td>670</td>
      <td>679</td>
      <td><a href="https://semantic.farm/mesh:D015994">mesh:D015994</a></td>
      <td>Incidence</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>683</td>
      <td>698</td>
      <td><a href="https://semantic.farm/mesh:D014946">mesh:D014946</a></td>
      <td>Wound Infection</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>734</td>
      <td>748</td>
      <td><a href="https://semantic.farm/mesh:D061646">mesh:D061646</a></td>
      <td>Operative Time</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>750</td>
      <td>768</td>
      <td><a href="https://semantic.farm/mesh:D010149">mesh:D010149</a></td>
      <td>Pain, Postoperative</td>
      <td>0.549</td>
    </tr>
    <tr>
      <td>770</td>
      <td>783</td>
      <td><a href="https://semantic.farm/mesh:D007902">mesh:D007902</a></td>
      <td>Length of Stay</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>785</td>
      <td>810</td>
      <td><a href="https://semantic.farm/mesh:D019106">mesh:D019106</a></td>
      <td>Postoperative Hemorrhage</td>
      <td>0.502</td>
    </tr>
    <tr>
      <td>825</td>
      <td>844</td>
      <td><a href="https://semantic.farm/mesh:D005769">mesh:D005769</a></td>
      <td>Gastrointestinal Motility</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>934</td>
      <td>940</td>
      <td><a href="https://semantic.farm/mesh:D010298">mesh:D010298</a></td>
      <td>Parity</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>942</td>
      <td>959</td>
      <td><a href="https://semantic.farm/mesh:D003920">mesh:D003920</a></td>
      <td>Diabetes Mellitus</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>965</td>
      <td>972</td>
      <td><a href="https://semantic.farm/mesh:D006664">mesh:D006664</a></td>
      <td>History</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>985</td>
      <td>1002</td>
      <td><a href="https://semantic.farm/mesh:D002585">mesh:D002585</a></td>
      <td>Cesarean Section</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1008</td>
      <td>1017</td>
      <td><a href="https://semantic.farm/mesh:D015994">mesh:D015994</a></td>
      <td>Incidence</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1021</td>
      <td>1044</td>
      <td><a href="https://semantic.farm/mesh:D013530">mesh:D013530</a></td>
      <td>Surgical Wound Infection</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>1181</td>
      <td>1204</td>
      <td><a href="https://semantic.farm/mesh:D061366">mesh:D061366</a></td>
      <td>Numbers Needed To Treat</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>1262</td>
      <td>1276</td>
      <td><a href="https://semantic.farm/mesh:D061646">mesh:D061646</a></td>
      <td>Operative Time</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1278</td>
      <td>1288</td>
      <td><a href="https://semantic.farm/mesh:D006400">mesh:D006400</a></td>
      <td>Hematocrit</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1295</td>
      <td>1299</td>
      <td><a href="https://semantic.farm/mesh:D010146">mesh:D010146</a></td>
      <td>Pain</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1307</td>
      <td>1320</td>
      <td><a href="https://semantic.farm/mesh:D007902">mesh:D007902</a></td>
      <td>Length of Stay</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>1326</td>
      <td>1345</td>
      <td><a href="https://semantic.farm/mesh:D005769">mesh:D005769</a></td>
      <td>Gastrointestinal Motility</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>1523</td>
      <td>1528</td>
      <td><a href="https://semantic.farm/mesh:D014930">mesh:D014930</a></td>
      <td>Women</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1542</td>
      <td>1557</td>
      <td><a href="https://semantic.farm/mesh:D014946">mesh:D014946</a></td>
      <td>Wound Infection</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1591</td>
      <td>1599</td>
      <td><a href="https://semantic.farm/mesh:D012106">mesh:D012106</a></td>
      <td>Research</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1648</td>
      <td>1668</td>
      <td><a href="https://semantic.farm/mesh:D017060">mesh:D017060</a></td>
      <td>Patient Satisfaction</td>
      <td>0.762</td>
    </tr>
  </tbody>
</table>

<p><strong>Associations of Perfluoroalkyl and Polyfluoroalkyl Substances With
Cardiovascular Disease Incidence in Adults With Prediabetes: Findings From the
Diabetes Prevention Program</strong>
(<a href="https://semantic.farm/pubmed:41413398">pubmed:41413398</a>)</p>

<blockquote>
  <p>Perfluoroalkyl and polyfluoroalkyl substances (PFAS) are persistent,
widespread environmental contaminants linked to cardiometabolic outcomes
including obesity, hyperlipidemia, and diabetes. We examined whether baseline
plasma PFAS concentrations are associated with incident cardiovascular disease
(CVD) in adults with prediabetes, leveraging data from DPPOS (Diabetes
Prevention Program Outcomes Study). Among 1382 participants, we quantified
baseline plasma concentrations of 6 PFAS. We used Cox proportional hazards
models to estimate the risks of developing CVD outcomes during a median of 21
years of follow-up for each PFAS and used quantile g-computation to evaluate
the joint effect of all 6 PFAS. Effect modification by age, sex, menopausal
status, diet, and physical activity was explored. The incidence of major
adverse cardiovascular events was 9.6%; 3.9% had CVD-related death. Each
increase in interquartile range (1.1 ng/mL) in 2-( In adults with prediabetes,
higher plasma concentrations of select PFAS, but not their mixture, were
prospectively associated with increased CVD risk. These findings underscore
PFAS as a potential environmental risk factor for CVD in high-risk
populations.</p>
</blockquote>

<table>
  <thead>
    <tr>
      <th>Start</th>
      <th>End</th>
      <th>CURIE</th>
      <th>Name</th>
      <th>Score</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>152</td>
      <td>159</td>
      <td><a href="https://semantic.farm/mesh:D009765">mesh:D009765</a></td>
      <td>Obesity</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>161</td>
      <td>175</td>
      <td><a href="https://semantic.farm/mesh:D006949">mesh:D006949</a></td>
      <td>Hyperlipidemias</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>220</td>
      <td>226</td>
      <td><a href="https://semantic.farm/mesh:D010949">mesh:D010949</a></td>
      <td>Plasma</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>276</td>
      <td>298</td>
      <td><a href="https://semantic.farm/mesh:D002318">mesh:D002318</a></td>
      <td>Cardiovascular Diseases</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>308</td>
      <td>314</td>
      <td><a href="https://semantic.farm/mesh:D000328">mesh:D000328</a></td>
      <td>Adult</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>320</td>
      <td>331</td>
      <td><a href="https://semantic.farm/mesh:D011236">mesh:D011236</a></td>
      <td>Prediabetic State</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>381</td>
      <td>388</td>
      <td><a href="https://semantic.farm/mesh:D019542">mesh:D019542</a></td>
      <td>Program</td>
      <td>0.778</td>
    </tr>
    <tr>
      <td>454</td>
      <td>460</td>
      <td><a href="https://semantic.farm/mesh:D010949">mesh:D010949</a></td>
      <td>Plasma</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>495</td>
      <td>526</td>
      <td><a href="https://semantic.farm/mesh:D016016">mesh:D016016</a></td>
      <td>Proportional Hazards Models</td>
      <td>0.549</td>
    </tr>
    <tr>
      <td>543</td>
      <td>548</td>
      <td><a href="https://semantic.farm/mesh:D012306">mesh:D012306</a></td>
      <td>Risk</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>679</td>
      <td>684</td>
      <td><a href="https://semantic.farm/mesh:D007596">mesh:D007596</a></td>
      <td>Joints</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>735</td>
      <td>738</td>
      <td><a href="https://semantic.farm/mesh:D012723">mesh:D012723</a></td>
      <td>Sex</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>759</td>
      <td>763</td>
      <td><a href="https://semantic.farm/mesh:D004032">mesh:D004032</a></td>
      <td>Diet</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>769</td>
      <td>786</td>
      <td><a href="https://semantic.farm/mesh:D015444">mesh:D015444</a></td>
      <td>Exercise</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>805</td>
      <td>814</td>
      <td><a href="https://semantic.farm/mesh:D015994">mesh:D015994</a></td>
      <td>Incidence</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>885</td>
      <td>890</td>
      <td><a href="https://semantic.farm/mesh:D003643">mesh:D003643</a></td>
      <td>Death</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>951</td>
      <td>957</td>
      <td><a href="https://semantic.farm/mesh:D000328">mesh:D000328</a></td>
      <td>Adult</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>963</td>
      <td>974</td>
      <td><a href="https://semantic.farm/mesh:D011236">mesh:D011236</a></td>
      <td>Prediabetic State</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>983</td>
      <td>989</td>
      <td><a href="https://semantic.farm/mesh:D010949">mesh:D010949</a></td>
      <td>Plasma</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1093</td>
      <td>1097</td>
      <td><a href="https://semantic.farm/mesh:D012306">mesh:D012306</a></td>
      <td>Risk</td>
      <td>0.762</td>
    </tr>
    <tr>
      <td>1159</td>
      <td>1170</td>
      <td><a href="https://semantic.farm/mesh:D012307">mesh:D012307</a></td>
      <td>Risk Factors</td>
      <td>0.54</td>
    </tr>
    <tr>
      <td>1192</td>
      <td>1203</td>
      <td><a href="https://semantic.farm/mesh:D011153">mesh:D011153</a></td>
      <td>Population</td>
      <td>0.762</td>
    </tr>
  </tbody>
</table>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="named entity recognition" /><category term="text mining" /><category term="natural language processing" /><category term="named entity normalization" /><category term="medical subject headings" /><category term="MeSH" /><category term="PubMed" /><category term="PyOBO" /><category term="SSSLM" /><summary type="html"><![CDATA[Annotating the literature with mentions of key concepts from a given domain is often the first step towards extracting more substantial structured knowledge. This can be challenging, as it typically encompasses acquiring and processing the relevant literature and ontologies then installing and applying difficult-to-use named entity recognition (NER) workflows. This post highlights software components I’ve implemented to simplify this workflow. I demonstrate it by annotating the biomedical literature available through PubMed with Medical Subject Headings (MeSH) terms, and also comment on how this can be generalized to other natural sciences, engineering, and humanities disciplines.]]></summary></entry><entry><title type="html">Machine-Actionable Training Materials at BioHackathon Germany 2025</title><link href="https://cthoyt.com/2025/12/09/biohackathon-de-2025.html" rel="alternate" type="text/html" title="Machine-Actionable Training Materials at BioHackathon Germany 2025" /><published>2025-12-09T11:08:00+00:00</published><updated>2025-12-09T11:08:00+00:00</updated><id>https://cthoyt.com/2025/12/09/biohackathon-de-2025</id><content type="html" xml:base="https://cthoyt.com/2025/12/09/biohackathon-de-2025.html"><![CDATA[<p>I recently attended the
<a href="https://www.denbi.de/de-nbi-events/1840-4th-biohackathon-germany">4<sup>th</sup> BioHackathon Germany</a>
hosted by the
<a href="https://www.denbi.de">German Network for Bioinformatics Infrastructure (de.NBI)</a>.
I participated in the project <em>On the Path to Machine-actionable Training
Materials</em> in order to improve the interoperability between
<a href="https://search.dalia.education/basic">DALIA</a>,
<a href="https://tess.elixir-europe.org">TeSS</a>,
<a href="https://elixirtess.github.io/mTeSS-X">mTeSS-X</a>, and
<a href="https://schema.org">Schema.org</a>. This post gives a summary of the activities
leading up to the hackathon and the results of our happy hacking.</p>

<h2 id="team">Team</h2>

<p><img src="/img/biohackathon2025/team.jpg" alt="" /></p>

<p>Our project,
<a href="https://www.denbi.de/de-nbi-events/1939-4th-biohackathon-germany-on-the-path-to-machine-actionable-training-materials">On the Path to Machine-actionable Training Materials</a>,
had the following active participants throughout the week:</p>

<ul>
  <li>Nick Juty &amp; Phil Reed (University of Manchester)</li>
  <li>Leyla Jael Castro &amp; Roman Baum (Deutsche Zentralbibliothek für Medizin; ZB
Med)</li>
  <li>Petra Steiner (University of Darmstadt)</li>
  <li>Oliver Knodel &amp; Martin Voigt (Helmholtz-Zentrum Dresden-Rossendorf; HZDR)</li>
  <li>Dilfuza Djamalova (Forschungszentrum Jülich; FZJ)</li>
  <li>Jacobo Miranda (European Molecular Biology Laboratory; EMBL)</li>
</ul>

<p>Nick and Petra were our team leaders and Phil acted as the project’s <em>de facto</em>
secretary. On the first day of the hackathon, we were briefly joined by Alban
Gaignard (Nantes University), Dimitris Panouris (SciLifeLab), and Harshita Gupta
(SciLifeLab) to present their current related work. Similarly, Dominik Brilhaus
(Heinrich-Heine-Universität Düsseldorf) joined on the first day to share his
perspective from DataPLANT (the NFDI consortium for plants) as a training
materials creator. Finally, Helena Schnitzer (FZJ) participated in some
Schema.org discussions through the week.</p>

<h2 id="goals">Goals</h2>

<p>We categorized our work plan into three streams:</p>

<ol>
  <li><a href="#training-material-interoperability"><strong>Training Material Interoperability</strong></a> -
survey the landscape of relevant ontologies and schemas for annotating
learning materials, curate mappings/crosswalks between existing data models,
develop a programmatic toolbox, and begin federating between training
material platforms</li>
  <li><a href="#training-material-analysis"><strong>Training Material Analysis</strong></a> - analyze
training materials at scale to group similar training materials, reduce
redundancy, and semi-automatically construct learning paths</li>
  <li><a href="#modeling-learning-paths"><strong>Modeling Learning Paths</strong></a> - collect use cases
and develop a (meta)data model for learning paths</li>
</ol>

<h2 id="training-material-interoperability">Training Material Interoperability</h2>

<p>Interoperability is third pillar of the
<a href="https://www.nature.com/articles/sdata201618">FAIR data principles</a>. Metadata
describing training materials may be captured and stored in one of several data
models including the DALIA Interchange Format (DIF) v1.3, the format implicitly
defined by the TeSS API, and the Schemas.org Learning Material profile. Further,
metadata records conforming to these data models are filled with references to
terms in other ontologies, controlled vocabularies, databases, and other
resources that mint (persistent) identifiers. Our overarching goal at the
hackathon was to improve interoperability on both levels.</p>

<h3 id="indexing-ontologies-and-schemas">Indexing Ontologies and Schemas</h3>

<p>Our first concrete goal for training material interoperability at the hackathon
was to survey ontologies, controlled vocabularies, databases, and other
resources that mint (persistent) identifiers that might appear in the metadata
describing a learning material. For example, TeSS uses the
<a href="https://semantic.farm/edam">EDAM Ontology</a> to annotate topics onto training
materials. For the same purpose, DALIA uses the
<a href="https://semantic.farm/kim.hcrt">Hochschulcampus Ressourcentypen</a> (I’ll say more
on how we deal with the conflicting resources in the section below on mappings).</p>

<p>Our second concrete goal was to survey schemas that are used in modeling open
educational resources and training materials, for example,
<a href="https://semantic.farm/sdo">Schema.org</a>,
<a href="https://semantic.farm/oerschema">OERSchema</a>, and
<a href="https://semantic.farm/modalia">MoDALIA</a>, which encodes the DALIA Interchange
Format (DIF) v1.3.</p>

<p>The Semantic Farm (<a href="https://semantic.farm">https://semantic.farm</a>) is
comprehensive database of metadata about resources that mint (persistent)
identifiers (e.g., ontologies, controlled vocabularies, databases, schemas) such
as their preferred CURIE prefix for usage in SPARQL queries and other semantic
web applications. It imports and aligns with other databases like
<a href="https://identifiers.org">Identifiers.org</a> (for the life sciences) and
<a href="https://bartoc.org">BARTOC</a> (for the digital humanities) to support
interoperability and sustainability. It follows the
<a href="https://www.nature.com/articles/s41597-024-03406-w">open data, open code, and open infrastructure (O3)</a>
guidelines and has well-defined governance to enable community maintenance and
support longevity.</p>

<p>It’s the perfect place to index all the learning material and open educational
resource-related ontologies, controlled vocabularies, databases, and schemas.</p>

<p>I gave a tutorial on how to search the Semantic Farm for ontologies, controlled
vocabularies and other resources that mint (persistent) identifiers, and how to
contribute any that are missing. In short, they can be contributed by filling
out the
<a href="https://github.com/biopragmatics/bioregistry/issues/new?template=new-prefix.yml">new prefix request template</a>
on GitHub. If you’re interested to add a new entry, you can directly use the
form, read the
<a href="https://github.com/biopragmatics/bioregistry/blob/main/docs/CONTRIBUTING.md#submitting-new-prefixes">contribution guidelines</a>,
or watch a
<a href="https://www.youtube.com/watch?v=e-I6rcV2_BE">short YouTube tutorial</a>.</p>

<p>While I had done some significant preparatory work before the hackathon by
creating many new entries in the Semantic Farm, the team found and added several
new and important entries to the Semantic Farm during the hackathon too. Here
are two highlights:</p>

<p><a href="https://orcid.org/0000-0001-5556-838X">Martin Voigt</a> contributed the prefix
<code class="language-plaintext highlighter-rouge">amb</code> for the
<a href="https://dini-ag-kim.github.io/amb/20231019">Allgemeines Metadatenprofil für Bildungsressourcen</a>
(General Metadata Profile for Educational Resources) in
<a href="https://github.com/biopragmatics/bioregistry/pull/1781">biopragmatics/bioregistry#1781</a>.
This is a metadata schema for learning materials produced by the
Kompetenzzentrum Interoperable Metadaten (KIM) within the Deutsche Initiative
für Netzwerkinformation e.V. that was heavily inspired by
<a href="https://schema.org">Schema.org</a> and the Dublin Core
<a href="https://www.dublincore.org/about/lrmi/">Learning Resource Metadata Initiative (LRMI)</a></p>

<p><a href="https://orcid.org/0009-0004-7782-2894">Dilfuza Djamalova</a> and
<a href="https://orcid.org/0009-0005-0673-021X">Jacobo Miranda</a> contributed the prefix
<code class="language-plaintext highlighter-rouge">gtn</code> for
<a href="https://training.galaxyproject.org/training-material">Galaxy Training Network</a>
training materials in
<a href="https://github.com/biopragmatics/bioregistry/pull/1779">biopragmatics/bioregistry#1779</a>.
This resource contains multi- and cross-disciplinary training materials for
using the Galaxy workflow management system. Below, I describe how we ingested
transformed the training materials from GTN into a common format such they can
be represented according to the DALIA Interchange Format (DIF) v1.3, the
implicit data model expected by TeSS, and in Schema.org-compliant RDF.</p>

<p>Ultimately, we collated relevant ontologies, controlled vocabularies, schemas
and other resources that mint (persistent) identifiers in a
<a href="https://semantic.farm/collection/0000018">collection</a> such that they can be
easily found and shared.</p>

<h3 id="semantic-mappings-and-crosswalks">Semantic Mappings and Crosswalks</h3>

<p><img src="/img/biohackathon2025/overlaps.svg" alt="" /></p>

<p>I alluded to the different resources used by TeSS and DALIA to annotate
disciplines. The issue of partially overlapping ontologies, controlled
vocabularies, and database is quite widespread, and can manifest in a few
different ways. The figure above shows that redundancy can arise because of
different focus within a domain (i.e., the chemistry example), different
hierarchical specificity (i.e., the disease example), and due to massive generic
resources having overlap across many domains (e.g., like UMLS, MeSH, NCIT).</p>

<p>This is problematic when integrating learning materials from different sources,
e.g., TeSS and DALIA, because two learning materials may be annotated with
different terms describing the same discipline. Therefore, the solution is to
create semantic mappings between these terms.</p>

<p>I’ve worked for several years on the
<a href="https://mapping-commons.github.io/sssom/">Simple Standard for Sharing Ontological Mappings (SSSOM)</a>
standard for storing semantic mappings, so this was naturally the target for our
work. Further, I have been working on a domain-agnostic workflow for predicting
semantic mappings with lexical matching and deploying a curation interface
called <a href="github.com/cthoyt/sssom-curator/">SSSOM Curator</a>. I gave a tutorial for
using SSSOM Curator to the team based on a previous tutorial I made (that can be
found on YouTube <a href="https://www.youtube.com/watch?v=FkXkOhT8gdc&amp;t=293s">here</a>). We
prepared predicted semantic mappings between several learning material-related
ontologies in
<a href="https://github.com/biopragmatics/biomappings/pull/204">biopragmatics/biomappings#204</a>,
but we didn’t prioritize semantic mapping curation during the hackathon. Here’s
what they look like in the SSSOM Curator interface for Biomappings:</p>

<p><img src="/img/biohackathon2025/sssom-curator-disciplines.png" alt="" /></p>

<p>Where curating correspondences between concepts in ontology, controlled
vocabularies, and databases is often called semantic mapping, curating
correspondences between schemas and properties therein is often called
crosswalks. We put a bigger emphasis on producing crosswalks between Schema.org
and MoDALIA. This is actually a more complex problem due to the fact that
correspondences between elements in schemas can be more sophisticated (e.g.,
mapping between two fields for first and last names to a single name field), but
there are at least a few places where properties can be mapped with SSSOM.</p>

<p><img src="/img/biohackathon2025/crosswalks.png" alt="" /></p>

<p>An interesting lesson learned is that some curators find using SKOS
relationships challenging because the narrow and broader relations have the
opposite direction than what they would expect. For example,
<code class="language-plaintext highlighter-rouge">X skos:narrowMatch Y</code> means that X is narrower than Y, not X has a narrow match
Y. Many vocabularies use a verb as part of the predicate to reduce this
confusion - I’m sure if it were <code class="language-plaintext highlighter-rouge">X skos:isNarrowMatchFor Y</code>, then this would not
have been a problem. Deep down, the real issue is that transparent identifiers
(i.e., human-readable ones) are bad, since they can’t be changed over time. See
the excellent article,
<a href="https://journals.plos.org/plosbiology/article?id=10.1371/journal.pbio.2001414">Identifiers for the 21<sup>st</sup> century</a>,
by McMurry <em>et al.</em> (2017) for a more detailed discussion on what makes a good
identifier.</p>

<h3 id="operationalizing-crosswalks">Operationalizing Crosswalks</h3>

<p>The next step was to translate the abstract crosswalks between DALIA, TeSS, and
Schema.org into a concrete implementation using a general purpose programming
language (i.e., Python).</p>

<h4 id="the-scaling-problem">The Scaling Problem</h4>

<p>Given that we only focused on these three data models, it’s not unrealistic to
produce a DALIA-TeSS crosswalk, TeSS-Schema.org crosswalk, and DALIA-Schema.org
crosswalk. However, this approach does not scale well - in general, it requires
curating and implementing ${N}\choose{2}$ crosswalks with $N$ being the number
of schemas.</p>

<p>An alternative is to use a hub-and-spoke model, in which one data model is
targeted as the intermediary used for interchange and storage. This reduces the
burden on curators of crosswalks, as they only have to curate a single crosswalk
for any given data model into the intermediary. Similarly, it reduces the burden
on code maintainers as only a single crosswalk has to be implemented.</p>

<p>The challenge with open educational resources and learning materials is that no
existing data model is sufficient to cover the (most important) aspects of all
other data models. This motivated us to implement a unified, generic data model
for learning materials to serve as the interoperability hub between DALIA, TeSS,
Schema.org, and other data models.</p>

<pre><code class="language-mermaid">graph TD
    subgraph alltoall ["All-to-All (complex, burdensome)"]
        dalia[DALIA] &lt;--&gt; tess[TeSS]
        dalia &lt;--&gt; schema[Schema.org]
        dalia &lt;--&gt; oerschema[OERschema]
        dalia &lt;--&gt; amb["Allgemeines Metadatenprofil für Bildungsressourcen (AMB)"]
        dalia &lt;--&gt; lrmi["Learning Resource Metadata Initiative (LRMI)"]
        dalia &lt;--&gt; erudite[ERuDIte]
        tess &lt;--&gt; schema
        tess &lt;--&gt; oerschema
        tess &lt;--&gt; amb
        tess &lt;--&gt; lrmi
        tess &lt;--&gt; erudite
        schema &lt;--&gt; oerschema
        schema &lt;--&gt; amb
        schema &lt;--&gt; lrmi
        schema &lt;--&gt; erudite
        oerschema &lt;--&gt; amb
        oerschema &lt;--&gt; lrmi
        oerschema &lt;--&gt; erudite
        amb &lt;--&gt; lrmi
        amb &lt;--&gt; erudite
        lrmi &lt;--&gt; erudite
    end

    subgraph hub ["Hub-and-Spoke (maintainable, extensible)"]
        direction TB
        hubn[Unified OER Data Model] &lt;--&gt; daliaspoke[DALIA]
        hubn[Unified OER Data Model] &lt;--&gt; tessspoke[TeSS]
        hubn[Unified OER Data Model] &lt;--&gt; schemaspoke[Schema.org]
        hubn[Unified OER Data Model] &lt;--&gt; oerschemaspoke[OERschema]
        hubn[Unified OER Data Model] &lt;--&gt; ambspoke["Allgemeines Metadatenprofil für Bildungsressourcen (AMB)"]
        hubn[Unified OER Data Model] &lt;--&gt; lrmispoke["Learning Resource Metadata Initiative (LRMI)"]
        hubn[Unified OER Data Model] &lt;--&gt; eruditespoke[ERuDIte]
    end

    alltoall --&gt; hub
</code></pre>

<p>The famous XKCD comic, <a href="https://xkcd.com/927">Standards (https://xkcd.com/927)</a>,
proselytizes that any proposal of a unified standard that covers everyone’s use
cases is doomed to be an $N+1$ competing standard. While I’m doing my best to
present the work done in preparation for the hackathon and at the hackathon in a
linear way, the truth is that most steps also included discussion, hacking,
trying, failing, and repeating. Therefore, I can confidently say that for
practical reasons, implementing a new <em>de facto</em> standard was the only realistic
choice.</p>

<h4 id="the-oerbservatory-data-model">The OERbservatory Data Model</h4>

<p><img src="/img/biohackathon2025/oerbservatory-schematic.png" alt="" /></p>

<p>During the hackathon, we implemented the open source
<a href="https://github.com/data-literacy-alliance/oerbservatory">OERbservatory</a> Python
package. I first want to talk about three major features that it includes:</p>

<ol>
  <li>a unified, generic
<a href="https://github.com/data-literacy-alliance/oerbservatory/blob/main/src/oerbservatory/model.py">object model</a>
for open educational resources that’s effectively the union of the best parts
of DALIA, TeSS, Schema.org, and a few other data models we found</li>
  <li>import and export to two open educational resource and learning materials
data models - DALIA and TeSS. We didn’t have time during the hackathon to
implement import and export to Schema.org.</li>
  <li>import from three external learning material repositories -
<a href="https://oerhub.at">OERhub</a>, <a href="https://oersi.org">OERSI</a>, and the
<a href="https://training.galaxyproject.org">Galaxy Training Network (GTN)</a></li>
</ol>

<p>Here’s an excerpt of the object model, implemented using
<a href="https://github.com/pydantic/pydantic">Pydantic</a>. Note that Pydantic uses a
combination of Python’s type system and type annotations to express constraints
and rules, similarly to how SHACL does. However, we get the benefit of Python
type checking and the Python runtime to check that we’ve encoded this all
correctly. Finally, all Pydantic models can be serialized and deserialized from
JSON.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">EducationalResource</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="s">"""Represents an educational resource."""</span>

    <span class="n">model_config</span> <span class="o">=</span> <span class="n">ConfigDict</span><span class="p">(</span><span class="n">arbitrary_types_allowed</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="n">reference</span><span class="p">:</span> <span class="n">Reference</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
        <span class="bp">None</span><span class="p">,</span>
        <span class="n">description</span><span class="o">=</span><span class="s">"The primary reference for this learning material"</span><span class="p">,</span>
        <span class="n">examples</span><span class="o">=</span><span class="p">[</span><span class="n">Reference</span><span class="p">(</span><span class="n">prefix</span><span class="o">=</span><span class="s">"dalia"</span><span class="p">,</span> <span class="n">identifier</span><span class="o">=</span><span class="s">""</span><span class="p">)]</span>
    <span class="p">)</span>
    <span class="n">title</span><span class="p">:</span> <span class="n">InternationalizedStr</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(...,</span> <span class="n">description</span><span class="o">=</span><span class="s">"The title of the learning material"</span><span class="p">)</span>
    <span class="n">authors</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Author</span> <span class="o">|</span> <span class="n">Organization</span><span class="p">]</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
        <span class="n">default_factory</span><span class="o">=</span><span class="nb">list</span><span class="p">,</span>
        <span class="n">description</span><span class="o">=</span><span class="s">"An ordered list of authors (i.e., persons or organizations) of the learning material"</span><span class="p">,</span>
        <span class="n">examples</span><span class="o">=</span><span class="p">[</span>
            <span class="n">Author</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s">"Charles Tapley Hoyt"</span><span class="p">,</span> <span class="n">orcid</span><span class="o">=</span><span class="s">"0000-0003-4423-4370"</span><span class="p">),</span>
            <span class="n">Organization</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s">"NFDI"</span><span class="p">,</span> <span class="n">ror</span><span class="o">=</span><span class="s">"05qj6w324"</span><span class="p">),</span>
        <span class="p">],</span>
        <span class="n">min_len</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="p">...</span>
</code></pre></div></div>

<details>
<summary>Technology Comparison (content warning: programming culture wars)</summary>
<p>
DALIA and Schema.org built on top of semantic web principles. Records about
learning materials encoded in these data models are stored in RDF and queryable
via SPARQL. However, while powerful, SPARQL is a querying language that is
inherently limited in its expressibility and utility. A general purpose
programming language is more suited for building data science workflows, search
engines, APIs, web interfaces, and other tools on top of open educational
resource and learning material data. That's why we emphasized concretizing the
crosswalks between DALIA, TeSS, and Schema.org in a software implementation.
</p><p>
We chose Python as the target language because of its ubiquity and ease of use.
When the TeSS platform was initially developed in the early 2010s, the Ruby
programming language and the Ruby on Rails framework were a popular choice for
developing web applications. Unfortunately, the scientific Python stack and
machine learning ecosystem led Python to being a clear winner for academics and
scientists. This creates an issue that only a small number of academics are
skilled in Ruby and can participate in the development of TeSS.
</p><p>
It was also crucial that we used Python such that our implementation was
reusable. For example, the DALIA 1.0 platform was implemented using Django,
which made it effectively impossible to reuse any of the underlying code
outside, e.g., in a data science workflow. The same issue is also true for the
TeSS implementation using Ruby-on-Rails. While these batteries-included
frameworks can get a minimal web application running quickly, they generally
lead developers towards writing code that isn't reusable.
</p>
</details>

<h4 id="oerbservatory-as-an-interoperability-hub-between-dalia-and-tess">OERbservatory as an Interoperability Hub between DALIA and TeSS</h4>

<p>Before we even started working on the OERbservatory, we had implemented two
packages for working with data in DALIA and TeSS:</p>

<ol>
  <li><a href="https://github.com/data-literacy-alliance/dalia-dif">data-literacy-alliance/dalia-dif</a>
implements a parser for the DALIA DIF v1.3 tabular format, an internal
representation of the content (also using Pydantic), and an RDF serializer
(using on <a href="https://github.com/cthoyt/pydantic-metamodel">pydantic-metamodel</a></li>
  <li><a href="https://github.com/cthoyt/tess-downloader">cthoyt/tess-downloader</a>
implements an API client to TeSS and an internal representation of the
learning resource data model (using Pydantic)</li>
</ol>

<p>Because each of these packages already implemented an internal (lossless)
representations of the data models for DALIA and TeSS, respectively, we only had
to write code in the OERbservatory that mapped the fields between them to
OERbservatory’s data model.</p>

<p>This was a <strong>big</strong> milestone towards interoperability. We demonstrated its
potential by programmatically downloading all learning materials from the ELIXIR
TeSS instance’s API and exporting them as DALIA RDF. Similarly, we converted all
learning materials curated for DALIA into the TeSS JSON format. Later, I’ll
describe how we took this workflow one step further to implement syncing between
DALIA and TeSS.</p>

<p>Note that this mapping can’t simply be expressed using SSSOM, SHACL, or other
declarative languages, because it relies on more sophisticated logic. For
example, topics annotated with ontology terms in the DALIA data model only store
the URI reference, whereas topics annotated with ontology terms in the TeSS data
model require both the URI reference and the term’s label. Since we’re encoding
our crosswalks using a general purpose programming language, we have a larger
toolkit available. Here, we could use
<a href="https://github.com/biopragmatics/pyobo">PyOBO</a>, a generic package I’ve written
for working with ontologies, for looking up labels.</p>

<p>Unfortunately, we did not have time to implement an importer/exporter for
Schema.org. We deprioritized this because Schema.org it felt the least
approachable due to the way its documentation is written, the complexity of its
models, and prolific use of mixins. We considered if we could automatically
generate Pydantic classes from Schema.org - and it turns out that
<a href="https://github.com/lexiq-legal/pydantic_schemaorg">pydantic-schemaorg</a> has
already done it! Unfortunately, the code is not compatible with modern versions
of Pydantic, and the project appears abandoned. We only had so much time at the
hackathon, so forking/reviving/rewriting <code class="language-plaintext highlighter-rouge">pydantic-schemaorg</code> was left as a task
for later.</p>

<h4 id="the-oerbservatory-as-an-aggregator">The OERbservatory as an Aggregator</h4>

<p>Besides open educational resources and learning materials that are encoded in
the DALIA, TeSS, and Schema.org formats, there are many repositories of learning
materials that do not conform to a well-defined schema. Prior to the hackathon,
I had already explored the Austrian <a href="https://oerhub.at">OERhub</a> and
<a href="https://oersi.org/resources">Open Educational Resources Search Index (OERSI)</a>
and written importers into <code class="language-plaintext highlighter-rouge">dalia-dif</code>. At the hackathon, I reimplemented those
importers using the newly formed OERbservatory unified, generic data model.</p>

<p>On the Thursday morning of the BioHackathon, I had an excellent
<a href="https://en.wikipedia.org/wiki/Team_programming#Mob_programming">mob programming</a>
session with <a href="https://orcid.org/0009-0004-7782-2894">Dilfuza Djamalova</a> and
<a href="https://orcid.org/0009-0005-0673-021X">Jacobo Miranda</a> to import training
materials from the
<a href="https://training.galaxyproject.org">Galaxy Training Network (GTN)</a>. It turns
out that there are already several open educational resources and learning
materials that are automatically scraped and imported by TeSS. However, those
importers are limited by TeSS’s relatively rigid data model, which is bound to
their database and can therefore not be easily evolved. Dilfuza and Jacobo had a
few goals for our hacking:</p>

<ul>
  <li>There are fields in GTN that aren’t yet captured by TeSS. They wanted to
implement those fields in OERbservatory, demonstrate their usage, then gently
nudge TeSS to evolve its data model to support their use cases</li>
  <li>They wanted to index their content in DALIA, which becomes much easier if they
only have to maintain one importer in OERbservatory which can already export
to DALIA</li>
  <li>GTN is part of the <a href="https://github.com/dekcd">DeKCD</a> consortia, which wants to
deduplicate training material. Adding an importer here gives access to the
workflows we’re building for reconciling different metadata curated in
different places about the same materials, and identifying similar materials
to reduce duplicate effort, and connect people working on the same kinds of
materials</li>
</ul>

<p>We implemented the GTN importer in
<a href="https://github.com/data-literacy-alliance/oerbservatory/pull/8">data-literacy-alliance/oerbservatory#8</a>
which covers tutorials in GTN and later could be extended to slide decks. Along
the way, we updated the main educational resource model in OERbservatory to
include a few new fields, including status (which also is shared by TeSS- that
now needs to be incorporated), the publication date, and the modified date. We
did not make a complete mapping for all fields in GTN due to time constraints,
so we implemented logging that summarizes fields that haven’t yet been mapped
(see the PR for examples of each). For example, the way that contributor
information is incorporated into the API from the frontmatter in the source is
interesting - it resolves the keys in the frontmatter to entries in
<a href="https://github.com/galaxyproject/training-material/blob/main/CONTRIBUTORS.yaml">this YAML file</a>
in the GTN GitHub repository. We will want to think about the best way to map
the authors into OERbservatory, and this also might be a time to extend the
author list to include contributor role annotations.</p>

<p>I was very excited that Dilfuza and Jacobo were motivated to work on this and
contribute following the hackathon. We see if the OERbservatory is approachable
enough for future external contributions! For example, Robert Hasse of
NFDI4BIOIMAGE already proactively prepared a script that exports their
consortium’s training materials into the DALIA DIF v1.3 tabular format. I don’t
consider this a very approachable format, and I’m sure efforts like his could
have been eased by using OERbservatory as a target. The next steps are to
incorporate the
<a href="https://www.dariah.ch">Swiss Digital Research Infrastructure for Arts and Humanities (DARIAH-CH)</a>
and <a href="https://www.psdi.ac.uk">Physical Sciences Data Infrastructure (PSDI)</a>
learning materials, which appeared on the schematic diagram for OERbservatory
earlier. There are also a lot of other potential learning material repositories
to scrape like Glittr.com. If you have a suggestion, you can drop it in the
<a href="https://github.com/data-literacy-alliance/oerbservatory/issues">OERbservatory issue tracker</a>.
Further, given that Martin Voigt was in the room during this hacking and
discussion, and he is the maintainer for TeSS’s
<a href="https://github.com/ElixirTeSS/TeSS_scrapers">scraper code</a>, we already started
formulating plans on how we might be able to deduplicate efforts.</p>

<h3 id="federation-of-open-educational-resources-and-learning-materials">Federation of Open Educational Resources and Learning Materials</h3>

<p><img src="/img/biohackathon2025/federation.svg" alt="" /></p>

<p>The next step towards interoperability beyond the demonstration of converting
between formats used by DALIA and TeSS was to demonstrate actually posting the
content to the live services.</p>

<p>While we are currently in the process of implementing submission of open
educational resources and learning materials in DALIA, TeSS already has a
web-based interface for
<a href="https://tess.elixir-europe.org/materials/new">registering new learning materials</a>.
TeSS doesn’t have a documented API endpoint for posting learning materials, but
luckily, Martin knew where it was and helped to figure out the correct way to
pass credentials to use it. We managed this by a combination of reading the Ruby
implementation of TeSS and good ‘ol trial and error. In the end, we implemented
posting learning materials in the TeSS-specific Python package in
<a href="https://github.com/cthoyt/tess-downloader/pull/2">cthoyt/tess-downloader#2</a>.
Then, it was only a matter of stringing together code that converts DALIA to
OERbservatory, OERbservatory to TeSS, and then to upload to TeSS.</p>

<p>In parallel, Martin worked on improving the devops behind the
<a href="https://tesshub.hzdr.de">PaNOSC TeSSHub</a> to enable quicky spinning up new TeSS
instances that each have their own subdomain. He created a different subdomain
for each of DALIA, OERSI, GTN/KCD, and OERhub. Finally, we wrote a script that
uploaded all open educational resources and learning material from each source
to the appropriate TeSS instance in
<a href="https://github.com/data-literacy-alliance/oerbservatory/pull/3">data-literacy-alliance/oerbservatory#3</a>.
The results in each space can be explored here:</p>

<table>
  <thead>
    <tr>
      <th>Source</th>
      <th>Domain</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>DALIA</td>
      <td><a href="https://dalia.tesshub.hzdr.de">https://dalia.tesshub.hzdr.de</a></td>
    </tr>
    <tr>
      <td>OERhub</td>
      <td><a href="https://oerhub.tesshub.hzdr.de">https://oerhub.tesshub.hzdr.de</a></td>
    </tr>
    <tr>
      <td>OERSI</td>
      <td><a href="https://oersi.tesshub.hzdr.de">https://oersi.tesshub.hzdr.de</a></td>
    </tr>
    <tr>
      <td>GTN/deKCD</td>
      <td><a href="https://kcd.tesshub.hzdr.de">https://kcd.tesshub.hzdr.de</a></td>
    </tr>
    <tr>
      <td>PanOSC</td>
      <td><a href="https://panosc.tesshub.hzdr.de">https://panosc.tesshub.hzdr.de</a></td>
    </tr>
  </tbody>
</table>

<p>A full list of spaces can be found
<a href="https://pan-training.tesshub.hzdr.de/spaces">here</a>.</p>

<h4 id="european-open-science-cloud">European Open Science Cloud</h4>

<p>The great specter looming over most NFDI-related projects is how to interface
with the European Open Science Cloud (EOSC). At the surface, EOSC is a massive
undertaking to democratize access to research infrastructure on the European
level. However, having just entered the NFDI bubble at the end of the summer, I
have bene overwhelmed by the high pressure to participate in EOSC combine with
the lack of funding and lack of direction on how to best go about doing that.
All of that being said, Oliver Knödel spend the hackathon preparing the concept
for how we could connect TeSSHub to the EOSC open educational resource and
training materials registry using the
<a href="https://www.openarchives.org/pmh/">Open Archives Initiative Protocol for Metadata Harvesting (OAI-PMH)</a>.
Once TeSSHub can demonstrate federating its content through this mechanism, we
can use as inspiration to make a generic implementation in OERbservatory.</p>

<h4 id="governance-and-provenance">Governance and Provenance</h4>

<p>Now that it’s possible to copy training materials from one platform to another,
we have started to consider governance and provenance issues like:</p>

<ul>
  <li>If a training material originally curated in DALIA is displayed in TeSS, how
is that attributed? We will have to carefully consider how metadata records
about learning resources are identified, and how those identifiers are passed
around during interchange/syncing.</li>
  <li>If a training material originally from TeSS is enriched in the DALIA platform,
should that information flow back to TeSS, and how? We will have to carefully
consider how information is deduplicated and reconciled</li>
  <li>How do we implement technical systems that can keep many federated platforms
up-to-date with each other?</li>
</ul>

<p>I’m sure there will be many more questions along these lines. Luckily, the
mTeSS-X group has already begun discussions on a smaller scale, since they care
about how to federate between many disparate TeSS instances.</p>

<h2 id="training-material-analysis">Training Material Analysis</h2>

<p>Our team split into two for the analysis of training materials. The first team
looked into algorithmic mechanisms for featurizing open educational resources
and learning materials and applications of those features. The second team
looked into using large language models (LLMs) for the automated construction of
learning paths.</p>

<h3 id="featurization-and-application">Featurization and Application</h3>

<p>The first team looked into two techniques for featurizing (i.e., assigning dense
vectors) to open educational resources and learning materials.</p>

<p>The first and most interpretable technique was to concatenate free text fields
and labels from structured fields from a learning resource and index the entire
corpus (i.e., all learning resources) using the
<a href="https://en.wikipedia.org/wiki/Tf%E2%80%93idf">term frequency-inverse document frequency (TF-IDF)</a>
algorithm. This does a small amount of text preprocessing, calculates a word
list for the entire corpus, then calculates for each word the likelihood of
appearance in a given learning material versus the entire corpus. Then, each
learning material is assigned a dense vector with values from $[0, 1]$ the
length of the word list. Learning materials can be compared, e.g., using cosine
similarity between their respective vectors.</p>

<p>The second technique was to use the <a href="https://sbert.net">sentence transformers</a>
machine learning architecture, which relies on a pre-trained (not large)
language model to accomplish a similar vectorization. Both methods run in less
than a few minutes for the corpus of learning resources from DALIA, TeSS,
OERHub, and OERSI. We also pre-calculated the all-by-all similarities and
applied a cutoff of 0.7 to shorten the list. Both the TF-IDF and sentence
transformers vector index and similarities are commit to the OERbservatory
repository and are available
<a href="https://github.com/data-literacy-alliance/oerbservatory/tree/main/output">here</a>.</p>

<p><img src="/img/biohackathon2025/similarities.png" alt="" /></p>

<p>After we had embeddings, Dilfuza began to investigate some of the following:</p>

<ol>
  <li>Identify duplicates metadata records corresponding to the same learning
material resource, e.g., when two different platforms scraped the same
learning material</li>
  <li>Semi-automatically identify similar training materials both to improve
suggestions to learners, to connect the learning material creators, and to
help de-duplicate training material creation efforts</li>
</ol>

<p>We only managed to get this far in the last day of the hackathon, so there is
still a lot more to do here! Originally, I had planned on also using these
embeddings to train classifiers for key provenance metadata such as topic,
target audience, and difficulty level, then to create a semi-automated curation
workflow for enriching learning materials whose records were sparse with
annotation. These will be next steps.</p>

<h3 id="automated-construction-of-learning-paths">Automated Construction of Learning Paths</h3>

<p>Nick looked into using large language models (LLMs) to construct learning paths
through machine-assisted dialog. This part is highly experimental so there isn’t
much to point to yet, but the idea was to take in a list of learning materials
(either hard-coded or as a URL for the chat system to retrieve) and a prompt to
ask the LLM ot collect similar materials base don objectives and keywords, then
create a learning path based on difficult (which is infrequently annotated) and
suggest a title.</p>

<p>This workflow was used to produce three learning paths on the following topics
that were each ordered, had reference links, a difficulty rating, a title, and
provider:</p>

<ol>
  <li>Sequencing and QC (10 items)</li>
  <li>Git and Version Control (6 items)</li>
  <li>Genome Annotation (8 items)</li>
</ol>

<p>More on this in future work!</p>

<h2 id="modeling-learning-paths">Modeling Learning Paths</h2>

<p>While there isn’t a clear consensus on what a learning path is, a simple
definition is that a learning path is a sequence of learning materials to
consume to help a learner achieve a specific level of competence on a topic.
TeSS implements a data model for learning paths based on this definition and the
ELIXIR TeSS instance has
<a href="https://tess.elixir-europe.org/learning_paths">eleven examples</a>. Our team had
the goal to develop an extension Schemas.org (in Bioschemas) to capture learning
paths.</p>

<p>For transparency, I didn’t actively participate in this track, but think it’s
worth sharing the results, most of which are adapted from Phil’s repository in
<a href="https://github.com/BioSchemas/LearningPath-sandbox">BioSchemas/LearningPath-sandbox</a>.</p>

<h3 id="proposed-data-model">Proposed Data Model</h3>

<p>Phil, Alban, and Leyla proposed two new Bioschemas profiles and a small change
to
<a href="https://bioschemas.org/profiles/TrainingMaterial/1.0-RELEASE">one Bioschemas profile</a>
with the help of Nick and Roman:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">LearningPath</code>: inherits from <code class="language-plaintext highlighter-rouge">Course</code></li>
  <li><code class="language-plaintext highlighter-rouge">LearningPathModule</code>: inherits from <code class="language-plaintext highlighter-rouge">Course</code>, <code class="language-plaintext highlighter-rouge">Syllabus</code>, <code class="language-plaintext highlighter-rouge">ListItem</code>, and
<code class="language-plaintext highlighter-rouge">ItemList</code></li>
  <li><code class="language-plaintext highlighter-rouge">TrainingMaterial</code>: inherits from <code class="language-plaintext highlighter-rouge">LearningResource</code> and <code class="language-plaintext highlighter-rouge">ListItem</code></li>
</ul>

<p>Here’s a class diagram describing the proposed data model, where 🔺 is
Schema.org type, 🟩 is Bioschemas profile, 🔵 is new profile:</p>

<pre><code class="language-mermaid">classDiagram
    direction TB
    class Event["Event🔺"] {
    }
    class CourseInstance["CourseInstance🔺🟩"] {
    }
    class Course["Course🔺🟩"] {
        syllabusSections
    }
    class new_LearningPath["new:LearningPath🔵"] {
        Syllabus[] syllabusSections
    }
    class ListItem["ListItem🔺"] {
        nextItem
    }
    class Syllabus["Syllabus🔺"] {
    }
    class new_LearningPathModule["new:LearningPathModule🔵"] {
        ListItem[] itemListElement
        LearningPathTopic nextItem
    }
    class LearningResource["LearningResource🔺"] {
    }
    class bio_TrainingMaterial["bio:TrainingMaterial🟩"] {
    }
    Course &lt;|-- new_LearningPath
    Course &lt;|-- new_LearningPathModule
    Syllabus &lt;|-- new_LearningPathModule
    ListItem &lt;|-- new_LearningPathModule
    LearningResource &lt;|-- Course
    LearningResource &lt;|-- bio_TrainingMaterial
    LearningResource &lt;|-- Syllabus
    Event &lt;|-- CourseInstance
</code></pre>

<h3 id="concrete-example-from-galaxy-training-network">Concrete Example from Galaxy Training Network</h3>

<p>The team mocked encoding the
<a href="https://tess.elixir-europe.org/learning_paths/introduction-to-galaxy-and-sequence-analysis-6384c0ed-3546-41cf-ac30-bff8680dd96c">Introduction to Galaxy and Sequence analysis</a>
learning path on TeSS in this new schema. This learning path has the following
structure:</p>

<ol>
  <li><strong>Module 1: Introduction to Galaxy</strong>
    <ol>
      <li>A short introduction to Galaxy</li>
      <li>Galaxy Basics for genomics</li>
    </ol>
  </li>
  <li><strong>Module 2: Basics of Genome Sequence Analysis</strong>
    <ol>
      <li>Quality Control</li>
      <li>Mapping</li>
      <li>An Introduction to Genome Assembly</li>
      <li>Chloroplast genome assembly</li>
    </ol>
  </li>
</ol>

<p>Here’s a mockup of how this could look in RDF:</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@prefix</span><span class="w"> </span><span class="nn">dct:</span><span class="w"> </span><span class="nl">&lt;http://purl.org/dc/terms/&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">ex:</span><span class="w"> </span><span class="nl">&lt;http://example.org/&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">schema:</span><span class="w"> </span><span class="nl">&lt;https://schema.org/&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="nn">ex:</span><span class="n">GA_learning_path</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">schema:</span><span class="n">Course</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">dct:</span><span class="n">conformsTo</span><span class="w"> </span><span class="nl">&lt;https://bioschemas.org/profiles/LearningPath&gt;</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">schema:</span><span class="n">courseCode</span><span class="w"> </span><span class="s">"GSA101"</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">schema:</span><span class="n">description</span><span class="w"> </span><span class="s">"This learning path aims to teach you the basics of Galaxy and analysis of sequencing data. "</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">schema:</span><span class="n">name</span><span class="w"> </span><span class="s">"Introduction to Galaxy and Sequence analysis"</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">schema:</span><span class="n">provider</span><span class="w"> </span><span class="nn">ex:</span><span class="n">ExampleUniversity</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">schema:</span><span class="n">syllabusSections</span><span class="w"> </span><span class="nn">ex:</span><span class="n">Module_1,
</span><span class="w">        </span><span class="nn">ex:</span><span class="n">Module_2</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="nn">ex:</span><span class="n">Module_1</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">schema:</span><span class="n">ItemList,
</span><span class="w">        </span><span class="nn">schema:</span><span class="n">ListItem,
</span><span class="w">        </span><span class="nn">schema:</span><span class="n">Syllabus</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">dct:</span><span class="n">conformsTo</span><span class="w"> </span><span class="nl">&lt;https://bioschemas.org/profiles/LearningPathModule&gt;</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">schema:</span><span class="n">itemListElement</span><span class="w"> </span><span class="nn">ex:</span><span class="n">TM11,
</span><span class="w">        </span><span class="nn">ex:</span><span class="n">TM12</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">schema:</span><span class="n">name</span><span class="w"> </span><span class="s">"Module 1: Introduction to Galaxy"</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">schema:</span><span class="n">nextItem</span><span class="w"> </span><span class="nn">ex:</span><span class="n">Module_2</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">schema:</span><span class="n">teaches</span><span class="w"> </span><span class="s">"Learn how to create a workflow"</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="nn">ex:</span><span class="n">TM11</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">schema:</span><span class="n">LearningResource,
</span><span class="w">        </span><span class="nn">schema:</span><span class="n">ListItem</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">dct:</span><span class="n">conformsTo</span><span class="w"> </span><span class="nl">&lt;https://bioschemas.org/profiles/TrainingMaterial&gt;</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">schema:</span><span class="n">description</span><span class="w"> </span><span class="s">"What is Galaxy"</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">schema:</span><span class="n">name</span><span class="w"> </span><span class="s">"(1.1) A short introduction to Galaxy"</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">schema:</span><span class="n">nextItem</span><span class="w"> </span><span class="nn">ex:</span><span class="n">TM12</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">schema:</span><span class="n">url</span><span class="w"> </span><span class="s">"https://tess.elixir-europe.org/materials/hands-on-for-a-short-introduction-to-galaxy-tutorial?lp=1%3A1"</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>Here’s the same thing from a graphical perspective:</p>

<pre><code class="language-mermaid">graph TD
    N1["Module 1: Introduction to Galaxy"]
    N3["(1.2) Galaxy Basics for genomics"]
    N1 -- itemListElement --&gt; N3
    N1["Module 1: Introduction to Galaxy"]
    N2["(1.1) A short introduction to Galaxy"]
    N1 -- itemListElement --&gt; N2
    N4["Module 2: Basics of Genome Sequence Analysis"]
    N8["(2.4) Chloroplast genome assembly"]
    N4 -- itemListElement --&gt; N8
    N2["(1.1) A short introduction to Galaxy"]
    N3["(1.2) Galaxy Basics for genomics"]
    N2 -- nextItem --&gt; N3
    N1["Module 1: Introduction to Galaxy"]
    N4["Module 2: Basics of Genome Sequence Analysis"]
    N1 -- nextItem --&gt; N4
    N7["(2.3) An Introduction to Genome Assembly"]
    N8["(2.4) Chloroplast genome assembly"]
    N7 -- nextItem --&gt; N8
    N4["Module 2: Basics of Genome Sequence Analysis"]
    N5["(2.1) Quality Control"]
    N4 -- itemListElement --&gt; N5
    N4["Module 2: Basics of Genome Sequence Analysis"]
    N6["(2.2) Mapping"]
    N4 -- itemListElement --&gt; N6
    N4["Module 2: Basics of Genome Sequence Analysis"]
    N7["(2.3) An Introduction to Genome Assembly"]
    N4 -- itemListElement --&gt; N7
    N6["(2.2) Mapping"]
    N7["(2.3) An Introduction to Genome Assembly"]
    N6 -- nextItem --&gt; N7
    N3["(1.2) Galaxy Basics for genomics"]
    N5["(2.1) Quality Control"]
    N3 -- nextItem --&gt; N5
    N5["(2.1) Quality Control"]
    N6["(2.2) Mapping"]
    N5 -- nextItem --&gt; N6
</code></pre>

<p>Something that I became aware of while listening to discussions about learning
path is the way that Schema.org models lists. I wonder why they don’t use the
built-in RDF notions of lists and instead implemented their own formalism. I saw
that this caused a lot of confusion for the team both during mocking and also
during SPARQL querying.</p>

<p>I think the next steps in terms of learning paths is to create a concrete
implementation in OERbsevatory - we have the benefit that the Python programming
language provides a much more ergonomic abstraction over lists and collections.
There’s a lot of content inside the Galaxy Training Network (GTN) that could be
ingested into such a learning path. Towards this end, I gave a quick demo of
Pydantic to the learning paths team and showed them how I typically go about
data modeling.</p>

<hr />

<p>I really enjoyed the BioHackathon, and in general, I am very happy to be
attending more events to network with other academics in Germany. It was totally
exhausting, too, which is why I didn’t manage to finish this in the week
following the event.</p>

<p>In other open educational resource and learning materials news, we pre-printed
the first ac academic article describing a specific use case for DALIA on arXiv
in September:
<a href="https://arxiv.org/abs/2509.18902">Teaching RDM in a smart advanced inorganic lab course and its provision in the DALIA platform</a>.
We’re currently finalizing a second article fully dedicated towards describing
the DALIA platform which I hope can go on the arXiv in early January. Stay
tuned!</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="LinkML" /><category term="Bioregistry" /><category term="prefix maps" /><category term="CURIEs" /><category term="URIs" /><summary type="html"><![CDATA[I recently attended the 4th BioHackathon Germany hosted by the German Network for Bioinformatics Infrastructure (de.NBI). I participated in the project On the Path to Machine-actionable Training Materials in order to improve the interoperability between DALIA, TeSS, mTeSS-X, and Schema.org. This post gives a summary of the activities leading up to the hackathon and the results of our happy hacking.]]></summary></entry><entry><title type="html">Extracting Semantic Mappings from BioPortal in SSSOM</title><link href="https://cthoyt.com/2025/11/23/sssom-from-bioportal.html" rel="alternate" type="text/html" title="Extracting Semantic Mappings from BioPortal in SSSOM" /><published>2025-11-23T11:08:00+00:00</published><updated>2025-11-23T11:08:00+00:00</updated><id>https://cthoyt.com/2025/11/23/sssom-from-bioportal</id><content type="html" xml:base="https://cthoyt.com/2025/11/23/sssom-from-bioportal.html"><![CDATA[<p>Earlier this week, a
<a href="https://obo-communitygroup.slack.com/archives/C0281J34Z6J/p1763636752508639">question</a>
was asked on OBO Foundry Slack on where to find semantic mappings to terms in
the
<a href="https://bioregistry.io/registry/snomedct">Systematized Nomenclature of Medicine - Clinical Terms (SNOMED-CT)</a>.
While some are available in the
<a href="https://doi.org/10.5281/zenodo.11091885">SeMRA Disease Mappings Database</a>,
there are many more available within BioPortal, which has access to the entire
SNOMED-CT source data and has produced semantic mapping predictions using
<a href="https://pubmed.ncbi.nlm.nih.gov/20351849">LOOM</a>. This post is about how I
implemented an API wrapper for generic OntoPortal instances’ mapping endpoints
and a post-processing pipeline that converts OntoPortal’s custom mapping format
into SSSOM.</p>

<h2 id="interacting-with-bioportal">Interacting with BioPortal</h2>

<p>BioPortal is an instance of a more generic backend called
<a href="https://ontoportal.org">OntoPortal</a>. I’ve previous developed
<a href="https://github.com/cthoyt/ontoportal-client"><code class="language-plaintext highlighter-rouge">ontoportal-client</code></a>, a Python
package that both has a generic wrapper for any OntoPortal’s API and
pre-configured wrappers for <a href="https://bioportal.bioontology.org">BioPortal</a>,
<a href="https://agroportal.lirmm.fr">AgroPortal</a>,
<a href="https://ecoportal.lifewatch.eu">EcoPortal</a>, and several others.</p>

<p>The OntoPortal API endpoint for retrieving mappings is <code class="language-plaintext highlighter-rouge">/mappings</code>, which takes
a comma separated pair of two ontologies as a parameter like in
<code class="language-plaintext highlighter-rouge">https://data.bioontology.org/mappings?apikey=&lt;API KEY&gt;&amp;ontologies=SNOMEDCT,AERO</code>.
I was able to relatively easily implement this in <code class="language-plaintext highlighter-rouge">ontoportal-client</code> in
<a href="https://github.com/cthoyt/ontoportal-client/pull/10">cthoyt/ontoportal-client#10</a>,
which enables automatically paging through results using the following code:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">ontoportal_client</span> <span class="kn">import</span> <span class="n">BioPortalClient</span>

<span class="c1"># follow https://github.com/cthoyt/ontoportal-client?tab=readme-ov-file#%EF%B8%8F-configuration
# to configure BioPortalClient to be instantiated without need for explicit configuration
</span><span class="n">client</span> <span class="o">=</span> <span class="n">BioPortalClient</span><span class="p">()</span>
<span class="k">for</span> <span class="n">record</span> <span class="ow">in</span> <span class="n">client</span><span class="p">.</span><span class="n">get_mappings</span><span class="p">(</span><span class="s">"SNOMEDCT"</span><span class="p">,</span> <span class="s">"AERO"</span><span class="p">):</span>
    <span class="k">pass</span>
</code></pre></div></div>

<p>Each <code class="language-plaintext highlighter-rouge">record</code> is a dictionary object corresponding to the JSON returned by the
API (after stripping pagination metadata):</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
  </span><span class="nl">"source"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LOOM"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"classes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"@id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://purl.obolibrary.org/obo/ogms/OMRE_0000023"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"@type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2002/07/owl#Class"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"links"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"self"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/AERO/classes/http%3A%2F%2Fpurl.obolibrary.org%2Fobo%2Fogms%2FOMRE_0000023"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"ontology"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/AERO"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"children"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/AERO/classes/http%3A%2F%2Fpurl.obolibrary.org%2Fobo%2Fogms%2FOMRE_0000023/children"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"parents"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/AERO/classes/http%3A%2F%2Fpurl.obolibrary.org%2Fobo%2Fogms%2FOMRE_0000023/parents"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"descendants"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/AERO/classes/http%3A%2F%2Fpurl.obolibrary.org%2Fobo%2Fogms%2FOMRE_0000023/descendants"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"ancestors"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/AERO/classes/http%3A%2F%2Fpurl.obolibrary.org%2Fobo%2Fogms%2FOMRE_0000023/ancestors"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"instances"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/AERO/classes/http%3A%2F%2Fpurl.obolibrary.org%2Fobo%2Fogms%2FOMRE_0000023/instances"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"tree"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/AERO/classes/http%3A%2F%2Fpurl.obolibrary.org%2Fobo%2Fogms%2FOMRE_0000023/tree"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"notes"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/AERO/classes/http%3A%2F%2Fpurl.obolibrary.org%2Fobo%2Fogms%2FOMRE_0000023/notes"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"mappings"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/AERO/classes/http%3A%2F%2Fpurl.obolibrary.org%2Fobo%2Fogms%2FOMRE_0000023/mappings"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"ui"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://bioportal.bioontology.org/ontologies/AERO?p=classes&amp;conceptid=http%3A%2F%2Fpurl.obolibrary.org%2Fobo%2Fogms%2FOMRE_0000023"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"@context"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"self"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2002/07/owl#Class"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"ontology"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://data.bioontology.org/metadata/Ontology"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"children"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2002/07/owl#Class"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"parents"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2002/07/owl#Class"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"descendants"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2002/07/owl#Class"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"ancestors"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2002/07/owl#Class"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"instances"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://data.bioontology.org/metadata/Instance"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"tree"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2002/07/owl#Class"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"notes"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://data.bioontology.org/metadata/Note"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"mappings"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://data.bioontology.org/metadata/Mapping"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"ui"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2002/07/owl#Class"</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"@context"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"@vocab"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://data.bioontology.org/metadata/"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"@language"</span><span class="p">:</span><span class="w"> </span><span class="s2">"en"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"@id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://purl.bioontology.org/ontology/SNOMEDCT/3415004"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"@type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2002/07/owl#Class"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"links"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"self"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/SNOMEDCT/classes/http%3A%2F%2Fpurl.bioontology.org%2Fontology%2FSNOMEDCT%2F3415004"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"ontology"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/SNOMEDCT"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"children"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/SNOMEDCT/classes/http%3A%2F%2Fpurl.bioontology.org%2Fontology%2FSNOMEDCT%2F3415004/children"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"parents"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/SNOMEDCT/classes/http%3A%2F%2Fpurl.bioontology.org%2Fontology%2FSNOMEDCT%2F3415004/parents"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"descendants"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/SNOMEDCT/classes/http%3A%2F%2Fpurl.bioontology.org%2Fontology%2FSNOMEDCT%2F3415004/descendants"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"ancestors"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/SNOMEDCT/classes/http%3A%2F%2Fpurl.bioontology.org%2Fontology%2FSNOMEDCT%2F3415004/ancestors"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"instances"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/SNOMEDCT/classes/http%3A%2F%2Fpurl.bioontology.org%2Fontology%2FSNOMEDCT%2F3415004/instances"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"tree"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/SNOMEDCT/classes/http%3A%2F%2Fpurl.bioontology.org%2Fontology%2FSNOMEDCT%2F3415004/tree"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"notes"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/SNOMEDCT/classes/http%3A%2F%2Fpurl.bioontology.org%2Fontology%2FSNOMEDCT%2F3415004/notes"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"mappings"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://data.bioontology.org/ontologies/SNOMEDCT/classes/http%3A%2F%2Fpurl.bioontology.org%2Fontology%2FSNOMEDCT%2F3415004/mappings"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"ui"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://bioportal.bioontology.org/ontologies/SNOMEDCT?p=classes&amp;conceptid=http%3A%2F%2Fpurl.bioontology.org%2Fontology%2FSNOMEDCT%2F3415004"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"@context"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"self"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2002/07/owl#Class"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"ontology"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://data.bioontology.org/metadata/Ontology"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"children"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2002/07/owl#Class"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"parents"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2002/07/owl#Class"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"descendants"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2002/07/owl#Class"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"ancestors"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2002/07/owl#Class"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"instances"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://data.bioontology.org/metadata/Instance"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"tree"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2002/07/owl#Class"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"notes"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://data.bioontology.org/metadata/Note"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"mappings"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://data.bioontology.org/metadata/Mapping"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"ui"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2002/07/owl#Class"</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"@context"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"@vocab"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://data.bioontology.org/metadata/"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"@language"</span><span class="p">:</span><span class="w"> </span><span class="s2">"en"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"process"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
  </span><span class="nl">"@id"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
  </span><span class="nl">"@type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://data.bioontology.org/metadata/Mapping"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>There’s both a lot of noise in this output and several pieces of key information
that need to be inferred. When designing <code class="language-plaintext highlighter-rouge">ontoportal-client</code> (and other similar
wrappers), I’ve had to grapple with staying true to the source, versus injecting
logic that processes and makes useful. For now, I’ve decided that
<code class="language-plaintext highlighter-rouge">ontoportal-client</code> shouldn’t make any judgments on the data that comes out of
the API. Also, since I wrote the package, the format has changed as well, and I
am not super interested in taking on that maintenance burden (which makes the
suggestion in
<a href="https://github.com/cthoyt/ontoportal-client/issues/3">cthoyt/ontoportal-client#3</a>)
difficult to address.</p>

<h2 id="converting-to-sssom">Converting to SSSOM</h2>

<p>If not in <code class="language-plaintext highlighter-rouge">ontoportal-client</code>, then where should I put the code that processes
OntoPortal mappings? I had two options. The first is in the Semantic Mapping
Reasoner and Assembler ( SeMRA; <a href="https://github.com/biopragmatics/semra">code</a>,
<a href="https://doi.org/10.1093/bioinformatics/btaf542">paper</a>), which is a generic
place for assembling semantic mappings. At the time, I designed the internal
data model in SeMRA to go beyond what’s possible in SSSOM because I was
interested in keeping track of provenance of how semantic mappings were used to
infer other ones. Slowly, I’m porting out the SSSOM-specific code from SeMRA
into a stand-alone library,
<a href="https://github.com/cthoyt/sssom-pydantic"><code class="language-plaintext highlighter-rouge">sssom-pydantic</code></a>. This serves as an
alternative to the <a href="https://github.com/mapping-commons/sssom-py"><code class="language-plaintext highlighter-rouge">sssom-py</code></a>
(which I also help maintain) that is more focused on creating a reusable and
high-performance data structure based on Pydantic.</p>

<p>Therefore, I implemented processing around a generic OntoPortal client in
<a href="https://github.com/cthoyt/sssom-pydantic/pull/14">cthoyt/sssom-pydantic#14</a>. It
can be used like this (warning: subject to change):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">bioregistry</span>
<span class="kn">from</span> <span class="nn">sssom_pydantic.contrib.ontoportal</span> <span class="kn">import</span> <span class="n">from_bioportal</span>
<span class="kn">from</span> <span class="nn">sssom_pydantic</span> <span class="kn">import</span> <span class="n">SemanticMapping</span>

<span class="n">converter</span> <span class="o">=</span> <span class="n">bioregistry</span><span class="p">.</span><span class="n">get_converter</span><span class="p">()</span>
<span class="n">mappings</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">SemanticMapping</span><span class="p">]</span> <span class="o">=</span> <span class="n">from_bioportal</span><span class="p">(</span><span class="s">"SNOMEDCT"</span><span class="p">,</span> <span class="s">"AERO"</span><span class="p">,</span> <span class="n">converter</span><span class="o">=</span><span class="n">converter</span><span class="p">)</span>
</code></pre></div></div>

<p>You have to bring your own <code class="language-plaintext highlighter-rouge">curies.Converter</code> because OntoPortal’s data model
doesn’t return a meaningful prefix map for parsing IRIs. The Bioregistry is a
good and quick way to get a comprehensive prefix map.</p>

<p>Warning: BioPortal doesn’t provide an option to only return mappings between
entities defined in the two given ontologies. For example, if you ask for
mappings between <code class="language-plaintext highlighter-rouge">SNOMEDCT</code> and <code class="language-plaintext highlighter-rouge">AERO</code>, you will also get mappings between OGMS
and SNOMEDCT (because OGMS terms are imported in AERO). This means that you
should probably apply post-hoc filtering to only retain relevant mappings.</p>

<p>One way to do this is to rely on the definition of the converter, since any
mappings with subject or objects with URIs that can’t be parsed are discarded:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">curies</span>
<span class="kn">from</span> <span class="nn">sssom_pydantic.contrib.ontoportal</span> <span class="kn">import</span> <span class="n">from_bioportal</span>

<span class="n">converter</span> <span class="o">=</span> <span class="n">curies</span><span class="p">.</span><span class="n">Converter</span><span class="p">.</span><span class="n">from_prefix_map</span><span class="p">(</span>
    <span class="p">{</span>
        <span class="s">"AERO"</span><span class="p">:</span> <span class="s">"http://purl.obolibrary.org/obo/AERO_"</span><span class="p">,</span>
        <span class="s">"SNOMEDCT"</span><span class="p">:</span> <span class="s">"http://purl.bioontology.org/ontology/SNOMEDCT/"</span><span class="p">,</span>
    <span class="p">}</span>
<span class="p">)</span>
<span class="n">mappings</span> <span class="o">=</span> <span class="n">from_bioportal</span><span class="p">(</span><span class="s">"SNOMEDCT"</span><span class="p">,</span> <span class="s">"AERO"</span><span class="p">,</span> <span class="n">converter</span><span class="o">=</span><span class="n">converter</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="bulk-download">Bulk Download</h2>

<p>Ideally, I could get <em>all</em> mappings from BioPortal in bulk, instead of needing
to hit the mappings API many times for each pair of two ontologies. The
motivation for this post originally came from a question on the OBO Foundry
Slack about where one could get SNOMED-CT mappings, so I wrote the following
script to go through all ontologies in the Bioregistry that have BioPortal
alignment to check for semantic mappings from SNOMED-CT to that mapping.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">bioregistry</span>
<span class="kn">import</span> <span class="nn">click</span>
<span class="kn">import</span> <span class="nn">pystow</span>
<span class="kn">import</span> <span class="nn">requests.exceptions</span>
<span class="kn">import</span> <span class="nn">sssom_pydantic</span>
<span class="kn">from</span> <span class="nn">sssom_pydantic</span> <span class="kn">import</span> <span class="n">MappingSet</span>
<span class="kn">from</span> <span class="nn">sssom_pydantic.contrib.ontoportal</span> <span class="kn">import</span> <span class="n">from_bioportal</span>
<span class="kn">from</span> <span class="nn">tqdm.contrib.logging</span> <span class="kn">import</span> <span class="n">logging_redirect_tqdm</span>
<span class="kn">from</span> <span class="nn">tqdm</span> <span class="kn">import</span> <span class="n">tqdm</span>

<span class="n">MODULE</span> <span class="o">=</span> <span class="n">pystow</span><span class="p">.</span><span class="n">module</span><span class="p">(</span><span class="s">"semra"</span><span class="p">,</span> <span class="s">"bioportal"</span><span class="p">)</span>
<span class="n">internal_to_bioportal</span> <span class="o">=</span> <span class="n">bioregistry</span><span class="p">.</span><span class="n">get_registry_map</span><span class="p">(</span><span class="s">"bioportal"</span><span class="p">)</span>
<span class="n">converter</span> <span class="o">=</span> <span class="n">bioregistry</span><span class="p">.</span><span class="n">get_converter</span><span class="p">()</span>

<span class="k">for</span> <span class="n">internal</span><span class="p">,</span> <span class="n">bioportal</span> <span class="ow">in</span> <span class="n">tqdm</span><span class="p">(</span><span class="nb">sorted</span><span class="p">(</span><span class="n">internal_to_bioportal</span><span class="p">.</span><span class="n">items</span><span class="p">())):</span>
    <span class="k">if</span> <span class="n">bioportal</span> <span class="o">==</span> <span class="s">"SNOMEDCT"</span><span class="p">:</span>
        <span class="k">continue</span>
    <span class="n">name</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"snomedct-</span><span class="si">{</span><span class="n">internal</span><span class="si">}</span><span class="s">.sssom.tsv"</span>
    <span class="n">path</span> <span class="o">=</span> <span class="n">MODULE</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="n">name</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">path</span><span class="p">.</span><span class="n">is_file</span><span class="p">():</span>
        <span class="n">tqdm</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">click</span><span class="p">.</span><span class="n">style</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">bioportal</span><span class="si">}</span><span class="s"> already cached to </span><span class="si">{</span><span class="n">path</span><span class="si">}</span><span class="s">"</span><span class="p">,</span> <span class="n">fg</span><span class="o">=</span><span class="s">"green"</span><span class="p">))</span>
        <span class="k">continue</span>
    <span class="n">tqdm</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">click</span><span class="p">.</span><span class="n">style</span><span class="p">(</span><span class="n">bioportal</span><span class="p">,</span> <span class="n">fg</span><span class="o">=</span><span class="s">"green"</span><span class="p">))</span>
    <span class="n">metadata</span> <span class="o">=</span> <span class="n">MappingSet</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="sa">f</span><span class="s">'https://w3id.org/biopragmatics/mappings/bioportal/</span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
    <span class="k">with</span> <span class="n">logging_redirect_tqdm</span><span class="p">():</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="n">mappings</span> <span class="o">=</span> <span class="n">from_bioportal</span><span class="p">(</span><span class="s">"SNOMEDCT"</span><span class="p">,</span> <span class="n">bioportal</span><span class="p">,</span> <span class="n">converter</span><span class="o">=</span><span class="n">converter</span><span class="p">)</span>
        <span class="k">except</span> <span class="n">requests</span><span class="p">.</span><span class="n">exceptions</span><span class="p">.</span><span class="n">HTTPError</span><span class="p">:</span>
            <span class="n">tqdm</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">click</span><span class="p">.</span><span class="n">style</span><span class="p">(</span><span class="sa">f</span><span class="s">"failed on </span><span class="si">{</span><span class="n">bioportal</span><span class="si">}</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">fg</span><span class="o">=</span><span class="s">"red"</span><span class="p">))</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">tqdm</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">click</span><span class="p">.</span><span class="n">style</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">bioportal</span><span class="si">}</span><span class="s"> got </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">mappings</span><span class="p">)</span><span class="si">:</span><span class="p">,</span><span class="si">}</span><span class="s"> mappings"</span><span class="p">,</span> <span class="n">fg</span><span class="o">=</span><span class="s">"green"</span><span class="p">))</span>
            <span class="k">if</span> <span class="n">mappings</span><span class="p">:</span>
                <span class="n">sssom_pydantic</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">mappings</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="n">converter</span><span class="o">=</span><span class="n">converter</span><span class="p">,</span> <span class="n">metadata</span><span class="o">=</span><span class="n">metadata</span><span class="p">)</span>
                <span class="n">tqdm</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">click</span><span class="p">.</span><span class="n">style</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">bioportal</span><span class="si">}</span><span class="s"> wrote to </span><span class="si">{</span><span class="n">path</span><span class="si">}</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">fg</span><span class="o">=</span><span class="s">"green"</span><span class="p">))</span>
</code></pre></div></div>

<p>As of writing, I haven’t been able to get this script to run to completion. The
BioPortal API is often slow and gives timeouts. I included caching so I could
resume after failure. As I mentioned earlier, this script doesn’t yet
post-process mappings to the correct subset.</p>

<hr />

<p>Thanks to John Graybeal for the suggestion on where to begin. He’s also helped
me get in touch with the BioPortal team, so hopefully we can collaborate to get
the API working using SSSOM directly or at least to get a bulk export of
mappings.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="BioPortal" /><category term="OntoPortal" /><category term="SSSOM" /><summary type="html"><![CDATA[Earlier this week, a question was asked on OBO Foundry Slack on where to find semantic mappings to terms in the Systematized Nomenclature of Medicine - Clinical Terms (SNOMED-CT). While some are available in the SeMRA Disease Mappings Database, there are many more available within BioPortal, which has access to the entire SNOMED-CT source data and has produced semantic mapping predictions using LOOM. This post is about how I implemented an API wrapper for generic OntoPortal instances’ mapping endpoints and a post-processing pipeline that converts OntoPortal’s custom mapping format into SSSOM.]]></summary></entry><entry><title type="html">Databases as Ontologies Part 1 - Background and Software</title><link href="https://cthoyt.com/2025/10/14/databases-as-ontologies-1-background.html" rel="alternate" type="text/html" title="Databases as Ontologies Part 1 - Background and Software" /><published>2025-10-14T08:14:00+00:00</published><updated>2025-10-14T08:14:00+00:00</updated><id>https://cthoyt.com/2025/10/14/databases-as-ontologies-1-background</id><content type="html" xml:base="https://cthoyt.com/2025/10/14/databases-as-ontologies-1-background.html"><![CDATA[<p>This is the first of a two-part post about encoding databases as ontologies. In
this post, I give a background on the problems in biocuration that led me to
start encoding databases as ontologies, the
<a href="https://github.com/biopragmatics/pyobo">software</a> I have written to do it, and
the <a href="https://github.com/biopragmatics/obo-db-ingest">repository</a> I have created
to store the resulting artifacts in a FAIR, open, and sustainable way. See also
the <a href="/2025/10/14/databases-as-ontologies-2-hgnc.html">second part</a>
which describes how I applied these tools to encode the
<a href="http://www.genenames.org">HGNC (HUGO Gene Nomenclature Committee)</a> database as
an ontology.</p>

<h2 id="motivation-for-pyobo">Motivation for PyOBO</h2>

<p>During my graduate studies, I was working with the
<a href="https://biological-expression-language.github.io">Biological Expression Language (BEL)</a>,
which curated causal, correlative, and associative relationships between
chemicals, proteins, diseases, and other biological entities. At the time, BEL
used a syntax similar to <a href="/2021/09/14/curies.html">CURIEs</a> for
referencing entities that had a (pseudo-)prefix and a string label for an
entity, which pointed to a BEL namespace file. Selventa had produced several
namespace files in 2015 (e.g., for GO, DO, HGNC) before <a href="/2020/04/28/history-of-bel.html">going out of
business</a>, but, because the namespace
files relied on names instead of identifiers, they quickly became out of date.
Naturally, I wanted to renew the existing BEL namespace files and also create
new ones for additional ontologies to support the curation I was doing.</p>

<p>With Selventa’s
<a href="https://github.com/OpenBEL/resource-generator"><em>ad hoc</em> scripts</a> as
inspiration, I set out on developing
<a href="https://github.com/biopragmatics/pyobo">PyOBO</a> to make unified access to both
ontologies and ontology-like resources (e.g., databases), both for the purposes
of making new BEL namespaces, but also for creating simple, unified access to
their contents. Initially, PyOBO wrapped Daniel Himmelstein’s
<a href="https://github.com/dhimmel/obonet">obonet</a> to read ontologies in the OBO flat
file format and constructed caches for efficient querying of fields like the
names, synonyms, and descriptions.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pyobo</span>

<span class="o">&gt;&gt;</span> <span class="o">&gt;</span> <span class="n">pyobo</span><span class="p">.</span><span class="n">get_name</span><span class="p">(</span><span class="s">"credit:software"</span><span class="p">)</span>
<span class="s">"Software"</span>
</code></pre></div></div>

<p>Since, PyOBO has grown to have more detailed and opinionated processing
workflows to handle the messy content curated across many ontologies, to be able
to handle additional formats (despite OBO being the most approachable ontology
format for outsiders, it also has the most issues associated with its
serialization and parsing), and to implement an internal domain-specific
language (DSL) for representing ontologies such that ontology-like resources
could be converted into ontology files.</p>

<h2 id="databases-as-ontologies">Databases as Ontologies</h2>

<p>The conversion of ontology-like resources into ontologies became increasingly
more interesting to me, especially because of my parallel interests in automatic
construction of knowledge graphs. I found that ontologies were a convenient
target for standardizing relationships, e.g., using the
<a href="https://bioregistry.io/ro">Relation Ontology (RO)</a>. Others were working on
similar problems in parallel, such as the OBO community’s
<a href="https://github.com/obophenotype/ncbitaxon/">converter for the NCBI Taxonomy Database</a>.
Notably, Chris Mungall gave a talk in 2021 entitled
<a href="https://doi.org/10.5281/zenodo.14661500">Limits of ontologies: How should databases be represented in OBO?</a>
which organized thoughts and ongoing challenges, most of which are still
relevant!</p>

<p>PyOBO now contains a suite of 60+
<a href="https://github.com/biopragmatics/pyobo/tree/main/src/pyobo/sources">sources</a>
for resources covering chemistry, biology, medicine, cultural heritage, the
semantic web, and other disciplines. Each source implements automated,
version-aware download and caching of data files from the resource and
transformation of the downloaded data into an OWL ontology within a concrete
instance of an
<a href="https://pyobo.readthedocs.io/en/latest/api/pyobo.Obo.html">abstract base class</a>
provided by PyOBO. Typically, resources make available tabular data in which
each row corresponds to a record in the resource that can either be transformed
into a <a href="https://www.w3.org/TR/owl-ref/#Class">class</a> or
<a href="https://www.w3.org/TR/owl-ref/#Individual">named individual</a> within an OWL
ontology. The remaining columns can often be mapped into other annotation
properties (i.e., metadata) or object properties (i.e., part of the logical
definition). For example, the label can be mapped to an annotation property
using <code class="language-plaintext highlighter-rouge">rdfs:label</code> and the species (if applicable) can be mapped to an object
property using <a href="https://bioregistry.io/RO:0002162"><code class="language-plaintext highlighter-rouge">RO:0002162</code></a> (in taxon).</p>

<p>Below is a demonstration of a minimal implementation of a PyOBO source for the
<a href="https://bioregistry.io/credit">CRediT (Contributor Roles Taxonomy)</a>, an
informally constructed controlled vocabulary for describing how authors
contributed to creative works. Note that the following is a relatively simple
PyOBO source that only uses a subset of PyOBO’s DSL for encoding ontology
components. The full version of this script is available in the PyOBO repository
<a href="https://github.com/biopragmatics/pyobo/blob/main/src/pyobo/sources/credit.py">here</a>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">more_itertools</span> <span class="kn">import</span> <span class="n">chunked</span>
<span class="kn">from</span> <span class="nn">pyobo.struct</span> <span class="kn">import</span> <span class="n">Obo</span><span class="p">,</span> <span class="n">Term</span>
<span class="kn">from</span> <span class="nn">pyobo.utils.path</span> <span class="kn">import</span> <span class="n">ensure_json</span><span class="p">,</span> <span class="n">ensure_open</span>

<span class="n">PREFIX</span> <span class="o">=</span> <span class="s">"credit"</span>
<span class="n">URI_PREFIX</span> <span class="o">=</span> <span class="s">"https://credit.niso.org/contributor-roles/"</span>
<span class="n">DATA_URL</span> <span class="o">=</span> <span class="s">"https://api.github.com/repos/CASRAI-CRedIT/Dictionary/contents/Picklists/Contributor%20Roles"</span>
<span class="n">ROOT_TERM</span> <span class="o">=</span> <span class="n">Term</span><span class="p">.</span><span class="n">from_triple</span><span class="p">(</span><span class="n">prefix</span><span class="o">=</span><span class="s">"cro"</span><span class="p">,</span> <span class="n">identifier</span><span class="o">=</span><span class="s">"0000000"</span><span class="p">)</span>


<span class="k">class</span> <span class="nc">CreditGetter</span><span class="p">(</span><span class="n">Obo</span><span class="p">):</span>
    <span class="n">ontology</span> <span class="o">=</span> <span class="n">PREFIX</span>
    <span class="n">static_version</span> <span class="o">=</span> <span class="s">"2022"</span>
    <span class="n">root_terms</span> <span class="o">=</span> <span class="p">[</span><span class="n">ROOT_TERM</span><span class="p">.</span><span class="n">reference</span><span class="p">]</span>

    <span class="k">def</span> <span class="nf">iter_terms</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">force</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">False</span><span class="p">):</span>
        <span class="k">yield</span> <span class="n">ROOT_TERM</span>
        <span class="k">for</span> <span class="n">records</span> <span class="ow">in</span> <span class="n">ensure_json</span><span class="p">(</span><span class="n">PREFIX</span><span class="p">,</span> <span class="n">url</span><span class="o">=</span><span class="n">DATA_URL</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"picklist-api.json"</span><span class="p">,</span> <span class="n">force</span><span class="o">=</span><span class="n">force</span><span class="p">):</span>
            <span class="k">with</span> <span class="n">ensure_open</span><span class="p">(</span><span class="n">PREFIX</span><span class="p">,</span> <span class="s">"picklist"</span><span class="p">,</span> <span class="n">url</span><span class="o">=</span><span class="n">records</span><span class="p">[</span><span class="s">"download_url"</span><span class="p">],</span> <span class="n">backend</span><span class="o">=</span><span class="s">"requests"</span><span class="p">,</span> <span class="n">force</span><span class="o">=</span><span class="n">force</span><span class="p">)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
                <span class="n">header</span><span class="p">,</span> <span class="o">*</span><span class="n">rest</span> <span class="o">=</span> <span class="nb">file</span><span class="p">.</span><span class="n">read</span><span class="p">().</span><span class="n">splitlines</span><span class="p">()</span>
                <span class="n">data</span> <span class="o">=</span> <span class="p">{</span><span class="n">key</span><span class="p">.</span><span class="n">removeprefix</span><span class="p">(</span><span class="s">"## "</span><span class="p">):</span> <span class="n">value</span> <span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">chunked</span><span class="p">(</span><span class="n">rest</span><span class="p">,</span> <span class="mi">2</span><span class="p">)}</span>
                <span class="n">term</span> <span class="o">=</span> <span class="n">Term</span><span class="p">.</span><span class="n">from_triple</span><span class="p">(</span>
                    <span class="n">prefix</span><span class="o">=</span><span class="n">PREFIX</span><span class="p">,</span>
                    <span class="n">identifier</span><span class="o">=</span><span class="n">data</span><span class="p">[</span><span class="s">"Canonical URL"</span><span class="p">].</span><span class="n">removeprefix</span><span class="p">(</span><span class="n">URI_PREFIX</span><span class="p">).</span><span class="n">rstrip</span><span class="p">(</span><span class="s">"/"</span><span class="p">),</span>
                    <span class="n">name</span><span class="o">=</span><span class="n">header</span><span class="p">.</span><span class="n">removeprefix</span><span class="p">(</span><span class="s">"# Contributor Roles/"</span><span class="p">),</span>
                    <span class="n">definition</span><span class="o">=</span><span class="n">data</span><span class="p">[</span><span class="s">"Short definition"</span><span class="p">],</span>
                <span class="p">)</span>
                <span class="n">term</span><span class="p">.</span><span class="n">append_parent</span><span class="p">(</span><span class="n">ROOT_TERM</span><span class="p">)</span>
                <span class="k">yield</span> <span class="n">term</span>
</code></pre></div></div>

<p>Each PyOBO source inherits a fully automated workflow for converting resources
into ontologies in the
<a href="https://owlcollab.github.io/oboformat/doc/GO.format.obo-1_4.html">OBO flat file format</a>
and
<a href="https://www.w3.org/TR/owl2-syntax/#Class_Expressions">OWL functional-style syntax (OFN)</a>.
Through OFN, PyOBO wraps <a href="https://github.com/owlcs/owlapi">OWLAPI</a> to enable
conversion to OWL/XML, RDF/XML,
<a href="https://github.com/geneontology/obographs/">OBO Graph JSON</a> and any other
supported format. As an aside: it’s a goal of mine to reimplement key parts of
OWLAPI in Python (or Rust) since it’s written in Java, which due to the
language’s waning popularity, poses a risk for OWLAPI’s further maintenance and
development.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pyobo.sources.credit</span> <span class="kn">import</span> <span class="n">CreditGetter</span>

<span class="n">ontology</span> <span class="o">=</span> <span class="n">CreditGetter</span><span class="p">()</span>
<span class="n">ontology</span><span class="p">.</span><span class="n">write_ofn</span><span class="p">(</span><span class="s">"credit.ofn"</span><span class="p">)</span>
<span class="n">ontology</span><span class="p">.</span><span class="n">write_obo</span><span class="p">(</span><span class="s">"credit.obo"</span><span class="p">)</span>
<span class="n">ontology</span><span class="p">.</span><span class="n">write_owl</span><span class="p">(</span><span class="s">"credit.owl"</span><span class="p">)</span>  <span class="c1"># enabled by OWLAPI
</span></code></pre></div></div>

<p>Here’s what the (abridged to only show a single term) OBO export looks like.
Full exports of CRediT in all formats (e.g., OFN, OBO, OWL/XML) can be found
<a href="https://github.com/biopragmatics/obo-db-ingest/tree/main/export/credit#readme">here</a>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>data-version: 2022
idspace: credit https://credit.niso.org/contributor-roles/ "Contributor Roles Taxonomy"
idspace: dcterms http://purl.org/dc/terms/ "Dublin Core Metadata Initiative Terms"
idspace: doap http://usefulinc.com/ns/doap# "Description of a Project"
idspace: foaf http://xmlns.com/foaf/0.1/ "Friend of a Friend"
idspace: orcid https://orcid.org/ "Open Researcher and Contributor"
ontology: credit
property_value: dcterms:title "Contributor Roles Taxonomy" xsd:string
property_value: dcterms:license "CC-BY-4.0" xsd:string
property_value: foaf:homepage "https\://credit.niso.org/" xsd:anyURI
property_value: doap:maintainer orcid:0000-0002-9298-3168
property_value: IAO:0000700 CRO:0000000

[Term]
id: credit:conceptualization
name: Conceptualization
def: "Ideas; formulation or evolution of overarching research goals and aims."
is_a: CRO:0000000
</code></pre></div></div>

<p>PyOBO implements several use case-specific exporters, such as exporting semantic
mappings in the
<a href="https://mapping-commons.github.io/sssom/">Simple Standard for Sharing Ontological Mappings (SSSOM)</a>,
exporting synonyms and literal mappings in the
<a href="https://github.com/cthoyt/ssslm">Simple Standard for Sharing Literal Mappings (SSSLM)</a>,
and nodes/edges files for import into the Neo4j graph database. It also
implements high-level workflows to support
<a href="https://pyobo.readthedocs.io/en/latest/ner.html">named entity recognition (NER) and named entity normalization (NEN)</a>,
and embedding entities using
<a href="https://pyobo.readthedocs.io/en/latest/api/pyobo.get_text_embeddings_df.html#pyobo.get_text_embeddings_df">(medium) language models</a>
or
<a href="https://pyobo.readthedocs.io/en/latest/api/pyobo.get_graph_embeddings_df.html#pyobo.get_graph_embeddings_df">graph machine learning</a>.</p>

<p>PyOBO has a deep integration with
<a href="https://github.com/biopragmatics/bioregistry">the Bioregistry</a> in order to
achieve consistent identifier standards. As such, it’s required that all sources
in PyOBO have a corresponding record in the Bioregistry, which covers key
metadata including the name, description, homepage, license, URI prefix, and
contact person for the resource. These fields are automatically propagated from
the Bioregistry into ontology metadata using appropriate annotation properties,
making ontologies produced by PyOBO much more transparent and understandable.</p>

<p>Because PyOBO is free and open source
<a href="https://github.com/biopragmatics/pyobo">on GitHub</a>, anyone can contribute
additional sources to the upstream repository. Alternatively, PyOBO has a plugin
system based on Python’s
<a href="https://packaging.python.org/en/latest/specifications/entry-points/">entry points</a>
such that additional sources can be implemented without making an upstream
contribution. This supports use cases such as when the resource is not publicly
available or when the code should not become publicly available (e.g., in the
context of a company). For example, some pharmaceutical companies use the plugin
system to implement sources for their compound registration system and internal
ontology system.</p>

<h2 id="obo-database-ingest">OBO Database Ingest</h2>

<p>The OBO Database Ingest (<code class="language-plaintext highlighter-rouge">obo-db-ingest</code>) is a
<a href="https://github.com/biopragmatics/obo-db-ingest">GitHub repository</a> that runs
the scripts for a subset of PyOBO sources whose data are permissively licensed,
and stores the OBO, OWL, OFN, OBO Graph JSON, SSSOM, SSSLM, and Neo4j files
(e.g, see the folder for
<a href="https://github.com/biopragmatics/obo-db-ingest/tree/main/export/mesh#readme">MeSH</a>).
It has a single Python script containing
<a href="https://peps.python.org/pep-0723/">PEP 723-compliant</a> inline script metadata
such that it is fully-self contained and can be run with <code class="language-plaintext highlighter-rouge">uv run</code>, assuming a
Java runtime is available for ROBOT and OWLAPI.</p>

<p>In theory, this script can be run on a chronological basis using GitHub Actions
to keep the outputs up-to-date. However, in practice, usually one or more
resources fail due to a combination of unreliable services (on the database’s
side) or issues where updates to the underlying data cause the scripts to fail.
I’m still working on making this script and PyOBO’s sources themselves more
resilient to such failures! One stop-gap could be to have smaller workflows
running on the most important resources that are updated frequently (e.g., HGNC
updates monthly) versus other resources which are updated infrequently (e.g.,
MeSH updates yearly). As an alternative, I often run this script locally, which
usually takes less than an hour because PyOBO cleverly caches versions, and the
script avoids duplicate work for data that hasn’t been updated.</p>

<p>The repository is structured such that there’s a consistent location for the
latest output of each PyOBO source as well as version-specific outputs. This
method indeed has its limits, since <code class="language-plaintext highlighter-rouge">git</code> is not really meant to be a file
storage system, especially for big files.</p>

<p>Because of its simple structure, it’s possible to assign persistent URLs (PURLs)
to each resource, configured by
<a href="https://github.com/perma-id/w3id.org/tree/master/biopragmatics">W3ID</a>. PURLs
abstract away the URL that points to a specific physical infrastructure required
for storing and serving files, making it possible to be changed later, and
therefore making resources more reliable to reference. As such, PURLs are an
often-requested feature by ontologies that would like to import and incorporate
PyOBO sources. They also enable the ontologies to be incorporated into tools
like the EBI’s Ontology Lookup Service (OLS), for example, I’ve already done
this for <a href="https://www.ebi.ac.uk/ols4/ontologies/mesh">MeSH</a>.</p>

<p>Here’s what a few PURLs look like for OBO files (replace the file extension with
<code class="language-plaintext highlighter-rouge">.ofn</code>, <code class="language-plaintext highlighter-rouge">.sssom.tsv</code>, etc. to get other resources):</p>

<table>
  <thead>
    <tr>
      <th>Resource</th>
      <th>Latest OBO PURL</th>
      <th>Version Type</th>
      <th>Example Versioned OBO PURL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://w3id.org/biopragmatics/resources/reactome#readme">Reactome</a></td>
      <td>https://w3id.org/biopragmatics/resources/reactome/reactome.obo</td>
      <td>Sequential</td>
      <td>https://w3id.org/biopragmatics/resources/reactome/83/reactome.obo</td>
    </tr>
    <tr>
      <td><a href="https://w3id.org/biopragmatics/resources/interpro#readme">Interpro</a></td>
      <td>https://w3id.org/biopragmatics/resources/interpro/interpro.obo</td>
      <td>Major/Minor</td>
      <td>https://w3id.org/biopragmatics/resources/interpro/92.0/interpro.obo</td>
    </tr>
    <tr>
      <td><a href="https://w3id.org/biopragmatics/resources/drugbank.salt#readme">DrugBank Salt</a></td>
      <td>https://w3id.org/biopragmatics/resources/drugbank.salt/drugbank.salt.obo</td>
      <td>Semantic</td>
      <td>https://w3id.org/biopragmatics/resources/drugbank.salt/5.1.9/drugbank.salt.obo</td>
    </tr>
    <tr>
      <td><a href="https://w3id.org/biopragmatics/resources/mesh#readme">MeSH</a></td>
      <td>https://w3id.org/biopragmatics/resources/mesh/mesh.obo.gz</td>
      <td>Year</td>
      <td>https://w3id.org/biopragmatics/resources/mesh/2023/mesh.obo.gz</td>
    </tr>
    <tr>
      <td><a href="https://w3id.org/biopragmatics/resources/uniprot#readme">UniProt</a></td>
      <td>https://w3id.org/biopragmatics/resources/uniprot/uniprot.obo.gz</td>
      <td>Year/Month</td>
      <td>https://w3id.org/biopragmatics/resources/uniprot/2022_05/uniprot.obo.gz</td>
    </tr>
    <tr>
      <td><a href="https://w3id.org/biopragmatics/resources/hgnc#readme">HGNC</a></td>
      <td>https://w3id.org/biopragmatics/resources/hgnc/hgnc.obo</td>
      <td>Date</td>
      <td>https://w3id.org/biopragmatics/resources/hgnc/2023-02-01/hgnc.obo</td>
    </tr>
    <tr>
      <td><a href="https://w3id.org/biopragmatics/resources/cgnc#readme">CGNC</a></td>
      <td>https://w3id.org/biopragmatics/resources/cgnc/cgnc.obo</td>
      <td>unversioned</td>
      <td>N/A</td>
    </tr>
  </tbody>
</table>

<p>The script also outputs a
<a href="https://github.com/biopragmatics/obo-db-ingest/raw/refs/heads/main/docs/_data/manifest.yml">full manifest</a>
that can be consumed by downstream resources that want to consume all content in
the repository, such as the <a href="https://kghub.org/kg-registry">KG Registry</a>.</p>

<hr />

<p>This post continues in a <a href="/2025/10/14/databases-as-ontologies-2-hgnc.html">second
part</a>, where I
describe how I implemented a more complex PyOBO source for the
<a href="http://www.genenames.org">HGNC (HUGO Gene Nomenclature Committee)</a> database.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="ontology" /><category term="OWL" /><category term="genes" /><summary type="html"><![CDATA[This is the first of a two-part post about encoding databases as ontologies. In this post, I give a background on the problems in biocuration that led me to start encoding databases as ontologies, the software I have written to do it, and the repository I have created to store the resulting artifacts in a FAIR, open, and sustainable way. See also the second part which describes how I applied these tools to encode the HGNC (HUGO Gene Nomenclature Committee) database as an ontology.]]></summary></entry><entry><title type="html">Databases as Ontologies Part 2 - A Case Study with HGNC</title><link href="https://cthoyt.com/2025/10/14/databases-as-ontologies-2-hgnc.html" rel="alternate" type="text/html" title="Databases as Ontologies Part 2 - A Case Study with HGNC" /><published>2025-10-14T08:14:00+00:00</published><updated>2025-10-14T08:14:00+00:00</updated><id>https://cthoyt.com/2025/10/14/databases-as-ontologies-2-hgnc</id><content type="html" xml:base="https://cthoyt.com/2025/10/14/databases-as-ontologies-2-hgnc.html"><![CDATA[<p>This is the second of a two-part post about encoding databases as ontologies. In
the <a href="/2025/10/14/databases-as-ontologies-1-background.html">first
part</a>, I gave a
background on how I started working on this problem and the software stack I
developed along the way. In this post, I explain the philosophy and design about
how I encoded the
<a href="http://www.genenames.org">HGNC (HUGO Gene Nomenclature Committee)</a> database as
an ontology using <a href="https://github.com/biopragmatics/pyobo">PyOBO</a>.</p>

<p>While the previous post used
<a href="https://bioregistry.io/registry/credit">CRediT (Contributor Roles Taxonomy)</a> to
demonstrate encoding as an ontology a simple resource that only contains names
and descriptions for its identifiers, the goal of this post is to describe the
design decisions take to ontologize a more complex resource: the
<a href="http://www.genenames.org">HGNC (HUGO Gene Nomenclature Committee) database</a>.</p>

<p>The HGNC assigns names, symbols, and numeric identifiers to human genes. Gene
symbols like
<a href="https://genenames.org/data/gene-symbol-report/#!/hgnc_id/HGNC:391">AKT1</a> are
the primary names referenced in the biomedical literature (which are sometimes
<a href="https://genomebiology.biomedcentral.com/articles/10.1186/s13059-016-1044-7">misunderstood by Microsoft Excel</a>).
HGNC identifiers are used to unambiguously reference human genes in biocuration
efforts like
<a href="https://www.deciphergenomics.org/gene/AKT1/overview/clinical-info">DECIPHER</a>,
<a href="https://vsearch.nlm.nih.gov/vivisimo/cgi-bin/query-meta?v%3Aproject=medlineplus&amp;query=AKT1">MedlinePlus</a>,
<a href="http://www.genecards.org/cgi-bin/carddisp.pl?id_type=hgnc&amp;id=391">GeneCards</a>,
and the
<a href="https://www.alliancegenome.org/gene/HGNC:391">Alliance of Genome Resources</a>.
They are also the targets of grounding human genes in manual literature curation
workflows (like for BEL, BioPAX, SBML) and text mining workflows (like
<a href="https://discovery.indra.bio">INDRA</a>).</p>

<p>I see the following three major benefits in ontologizing HGNC:</p>

<ol>
  <li>To support the standardized reuse of HGNC terms within semantic web
applications and ontologies. While databases can create fields with
well-defined semantics where they place either numeric HGNC identifiers or
references to HGNC gene symbols, semantic web applications often require the
use of (consistent) URIs and ontologies further require consistent as
classes/individuals with the appropriate axioms. For example, the
<a href="https://bioregistry.io/mondo">MONDO Disease Ontology</a> annotates genes’
relationships to disease (such as being a disease driver), but they are
forced to use workarounds to reference HGNC records, since they are not
encoded in an ontology.</li>
  <li>To support the standardized distribution of HGNC. HGNC has its own <em>ad hoc</em>
distribution formats (JSON, SQL, TSV). Ontologizing HGNC enables standard
tooling to consume and reuse the database.</li>
  <li>To support the standardized interpretation of HGNC. The content of HGNC does
not have formally defined semantics - for example, if you download the JSON
dump, how does one know what the <code class="language-plaintext highlighter-rouge">mane_select</code> key means, or what
<code class="language-plaintext highlighter-rouge">virus integration site</code> means in the <code class="language-plaintext highlighter-rouge">locus_type</code> field? Ontologizing HGNC
enables for a single person or small group to do the hard work of
understanding the meaning of the fields and values used in the source data,
then encode their hard-earned domain knowledge with formal semantics such
that everyone can understand it. I’ll use the <code class="language-plaintext highlighter-rouge">locus_type</code> and <code class="language-plaintext highlighter-rouge">locus_group</code>
fields as an example to illustrate this.</li>
</ol>

<p>I don’t want to bury the lede, so here’s a link to the
<a href="https://github.com/biopragmatics/pyobo/blob/main/src/pyobo/sources/hgnc/hgnc.py">PyOBO source script for HGNC</a>
that implements everything I’m about to describe. Actionable feedback and pull
requests are welcome if you have concrete ideas for improvement.</p>

<h2 id="lexicalization-of-a-gene">Lexicalization of a Gene</h2>

<p>Each record contains up to five lexical components (i.e., name, description,
synonyms), which are mapped to the ontology as follows:</p>

<table>
  <thead>
    <tr>
      <th>Key</th>
      <th>Cardinality</th>
      <th>Predicate</th>
      <th>Synonym Type</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">symbol</code></td>
      <td>one</td>
      <td><code class="language-plaintext highlighter-rouge">rdfs:label</code></td>
      <td>N/A</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">name</code></td>
      <td>one</td>
      <td><code class="language-plaintext highlighter-rouge">dcterms:description</code></td>
      <td>N/A</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">alias symbol</code></td>
      <td>zero or more</td>
      <td><code class="language-plaintext highlighter-rouge">oboInOwl:hasExactSynonym</code></td>
      <td><code class="language-plaintext highlighter-rouge">OMO:0003016</code> (gene symbol synonym)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">alias_name</code></td>
      <td>zero or more</td>
      <td><code class="language-plaintext highlighter-rouge">oboInOwl:hasExactSynonym</code></td>
      <td><code class="language-plaintext highlighter-rouge">OMO:0003008</code> (previous name)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">previous_symbol</code></td>
      <td>zero or more</td>
      <td><code class="language-plaintext highlighter-rouge">oboInOwl:hasExactSynonym</code></td>
      <td><code class="language-plaintext highlighter-rouge">OMO:0003015</code> (previous gene symbol)</td>
    </tr>
  </tbody>
</table>

<p>The dichotomy of gene symbols (short form) and gene names (long form) requires a
making the important design decision of which to use as the label. I chose to
use the gene symbol because of its ubiquitous use, see discussion
<a href="https://github.com/information-artifact-ontology/ontology-metadata/pull/197#discussion_r2428235955">here</a>.
An alternative to this lexicalization could be to mark the <code class="language-plaintext highlighter-rouge">name</code> as the primary
label with <code class="language-plaintext highlighter-rouge">rdfs:label</code> and to use the <code class="language-plaintext highlighter-rouge">symbol</code> as an exact synonym with type
<a href="https://bioregistry.io/OMO:0003000">abbreviation <code class="language-plaintext highlighter-rouge">OMO:0003000</code></a>. However, using
the gene symbol as the primary label is so ubiquitous that this seemed
appropriate. Further, HGNC does not provide dedicated textual descriptions, and
in their place, the name is often a reasonable alternative.</p>

<p>Here’s an example record in OBO flat file format to illustrate:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Term]
id: hgnc:100
name: ASIC1
def: "acid sensing ion channel subunit 1"
synonym: "ACCN2" RELATED OMO:0003015 []
synonym: "BNaC2" RELATED OMO:0003016 []
synonym: "acid sensing (proton gated) ion channel 1" RELATED OMO:0003008 []
synonym: "acid-sensing (proton-gated) ion channel 1" RELATED OMO:0003008 []
synonym: "amiloride-sensitive cation channel 2, neuronal" RELATED OMO:0003008 []
synonym: "hBNaC2" RELATED OMO:0003016 []
</code></pre></div></div>

<p>As an aside: the classes and properties needed to curate an ontology, or
ontologize a database, aren’t always available from the start. In many
situations, this leads to making <em>ad hoc</em> classes or properties to get the job
done - I am not above this. Initially, I had created <em>ad hoc</em> synonym types for
gene symbol synonyms and previous gene symbols. Later, I
<a href="https://github.com/information-artifact-ontology/ontology-metadata/pull/197">requested two new synonym types</a>
in the <a href="https://bioregistry.io/omo">OBO Metadata Ontology (OMO)</a> to cover these
use cases. This is actually a difficult step, because it requires justifying to
the community why they are useful. In this case, I think it’s clear, since all
model organism databases (MODs) make these kinds of synonyms, and I was able to
give a good justification based on the fact that I also made similar <em>ad hoc</em>
synonym types for the PyOBO source for the Rat Genome Database (RGD). After
doing the design work and making the pull request, I updated both the HGNC and
RGD sources in PyOBO to reuse these terms in
<a href="https://github.com/biopragmatics/pyobo/pull/447">biopragmatics/pyobo#447</a>.</p>

<h2 id="classification-of-a-gene-by-locus-type">Classification of a Gene by Locus Type</h2>

<p>Each gene is annotated with a locus group and locus type. These correspond to a
classification, which translates into an ontology as parent-child relationships
between classes, mediated by the <code class="language-plaintext highlighter-rouge">rdfs:subClassOf</code> relationship (often
abbreviated by <em>is a</em>). Here’s a count summary of all locus groups at the time
of writing:</p>

<table>
  <thead>
    <tr>
      <th>Locus Type</th>
      <th style="text-align: right">Frequency</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>protein-coding gene</td>
      <td style="text-align: right">19,297</td>
    </tr>
    <tr>
      <td>pseudogene</td>
      <td style="text-align: right">14,602</td>
    </tr>
    <tr>
      <td>non-coding RNA</td>
      <td style="text-align: right">9,634</td>
    </tr>
    <tr>
      <td>other</td>
      <td style="text-align: right">1,004</td>
    </tr>
  </tbody>
</table>

<p>Here’s a count summary of all locus types at the time of writing. It’s clear
that the locus type is more granular than locus group and completely subsumes
it. Therefore, I’ll throw away the locus group and only discuss the locus type
here. Looking ahead, I’ve included my manual mapping from each <em>ad hoc</em> values
used in HGNC to formal terms in the
<a href="https://bioregistry.io/so">Sequence Ontology (SO)</a>.</p>

<table>
  <thead>
    <tr>
      <th>Locus Type</th>
      <th style="text-align: right">Frequency</th>
      <th>Sequence Ontology</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>gene with protein product</td>
      <td style="text-align: right">19,297</td>
      <td>SO:0001217</td>
    </tr>
    <tr>
      <td>pseudogene</td>
      <td style="text-align: right">14,361</td>
      <td>SO:0000336</td>
    </tr>
    <tr>
      <td>RNA, long non-coding</td>
      <td style="text-align: right">6,296</td>
      <td>SO:0002127</td>
    </tr>
    <tr>
      <td>RNA, micro</td>
      <td style="text-align: right">1,912</td>
      <td>SO:0001265</td>
    </tr>
    <tr>
      <td>RNA, transfer</td>
      <td style="text-align: right">591</td>
      <td>SO:0001272</td>
    </tr>
    <tr>
      <td>RNA, small nucleolar</td>
      <td style="text-align: right">568</td>
      <td>SO:0001267</td>
    </tr>
    <tr>
      <td>immunoglobulin gene</td>
      <td style="text-align: right">230</td>
      <td>SO:0002122</td>
    </tr>
    <tr>
      <td>T cell receptor gene</td>
      <td style="text-align: right">205</td>
      <td>SO:0002133</td>
    </tr>
    <tr>
      <td>immunoglobulin pseudogene</td>
      <td style="text-align: right">203</td>
      <td>SO:0002098</td>
    </tr>
    <tr>
      <td>readthrough</td>
      <td style="text-align: right">151</td>
      <td>SO:0000697</td>
    </tr>
    <tr>
      <td>RNA, cluster</td>
      <td style="text-align: right">119</td>
      <td>SO:0003001 (see <a href="https://github.com/The-Sequence-Ontology/SO-Ontologies/pull/667">PR</a>)</td>
    </tr>
    <tr>
      <td>fragile site</td>
      <td style="text-align: right">116</td>
      <td>SO:0002349</td>
    </tr>
    <tr>
      <td>endogenous retrovirus</td>
      <td style="text-align: right">110</td>
      <td>SO:0000100</td>
    </tr>
    <tr>
      <td>unknown</td>
      <td style="text-align: right">69</td>
      <td>SO:0000704 (mapped to top-level gene)</td>
    </tr>
    <tr>
      <td>complex locus constituent</td>
      <td style="text-align: right">69</td>
      <td>SO:0000997</td>
    </tr>
    <tr>
      <td>RNA, ribosomal</td>
      <td style="text-align: right">60</td>
      <td>SO:0001637</td>
    </tr>
    <tr>
      <td>RNA, small nuclear</td>
      <td style="text-align: right">51</td>
      <td>SO:0001268</td>
    </tr>
    <tr>
      <td>region</td>
      <td style="text-align: right">46</td>
      <td>SO:0001411</td>
    </tr>
    <tr>
      <td>T cell receptor pseudogene</td>
      <td style="text-align: right">38</td>
      <td>SO:0002099</td>
    </tr>
    <tr>
      <td>RNA, misc</td>
      <td style="text-align: right">29</td>
      <td>SO:0001266</td>
    </tr>
    <tr>
      <td>virus integration site</td>
      <td style="text-align: right">8</td>
      <td>SO:0003002 (see <a href="https://github.com/The-Sequence-Ontology/SO-Ontologies/pull/668">PR</a>)</td>
    </tr>
    <tr>
      <td>RNA, Y</td>
      <td style="text-align: right">4</td>
      <td>SO:0002359</td>
    </tr>
    <tr>
      <td>RNA, vault</td>
      <td style="text-align: right">4</td>
      <td>SO:0002358</td>
    </tr>
  </tbody>
</table>

<p>I created <a href="https://github.com/biopragmatics/pyobo/issues/118">this issue</a> on the
PyOBO tracker when I started preparing this mapping, since there were a few
already available in the info box for the locus type on a given gene page on the
HGNC website. However, several were incorrect and most were missing. Therefore,
I had to manually map several to terms in the Sequence Ontology. Many mappings
were easy, but several required discussion with the HGNC and Sequence Ontology
teams (as you can see on the issue). HGNC was proactive and incorporated my
mappings into their front-end.</p>

<p>There were several cases where there was no appropriate term in the Sequence
Ontology. For some, the maintainers created new terms. Unfortunately, for some,
the maintainers were unresponsive, so I had to make my own PRs to the repository
which probably won’t get accepted in a timely fashion. However, I was able to
use the placeholder identifiers in the PyOBO source module even though they
haven’t yet been merged and released.</p>

<p>As an aside: annotating locus types is not just a human gene problem, but all
model organism databases (MODs) need to work on. I already have a
<a href="https://github.com/biopragmatics/pyobo/issues/165">thread</a> for taking a similar
approach for FlyBase, but it would be great to do the same for MGI (mouse), RGD
(rat), and other MODs for which PyOBO encodes a source. In general, it would be
great to see the Alliance of Genome Resources (AGR) push their members towards
adopting more shared semantics in the way they curate, especially for locus
types.</p>

<h2 id="chromosomal-locations">Chromosomal Locations</h2>

<p>The <code class="language-plaintext highlighter-rouge">location</code> field connects a gene to its chromosomal location by encoding the
location as a string. Initially, I had created an <em>ad hoc</em> relation to encode
this string field (<code class="language-plaintext highlighter-rouge">obo:hgnc#has_location</code>). In
<a href="https://github.com/biopragmatics/pyobo/pull/451">biopragmatics/pyobo#451</a>, I
adapted this to map the chromosomal location strings to classes in the
<a href="https://bioregistry.io/chr">Chromosome Ontology (CHR)</a> and use a combination of
well-established relations, based on the apparent values for the chromosomes.
Note that this is a first attempt at ontologization, and the relations might
need updating.</p>

<h3 id="single-point-annotations">Single Point Annotations</h3>

<p><a href="https://bioregistry.io/RO:0001025">RO:0001025 (located in)</a> is used for single
point annotations, such as in <a href="https://bioregistry.io/hgnc:10080">hgnc:10080</a>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Term]
id: hgnc:10080
name: RNPS1
is_a: SO:0001217 ! protein_coding_gene
relationship: RO:0001025 CHR:9606-chr16p13.3 ! located in 16p13.3 (Human)
</code></pre></div></div>

<h3 id="pairs-of-points">Pairs of Points</h3>

<p>Multiple <a href="https://bioregistry.io/RO:0001025">RO:0001025 (located in)</a> is used
for pairs of point annotations, e.g., when written like <code class="language-plaintext highlighter-rouge">Xq28 and chrYq12</code>, like
in <a href="https://bioregistry.io/hgnc:38513">hgnc:38513</a>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>id: hgnc:38513
name: WASIR1
is_a: SO:0002127 ! lncRNA_gene
relationship: RO:0001025 CHR:9606-chrXq28 ! located in Xq28 (Human)
relationship: RO:0001025 CHR:9606-chrYq12 ! located in Yq12 (Human)
</code></pre></div></div>

<p>There’s also a single example of a location containing an “or” in
<a href="https://bioregistry.io/hgnc:3829">hgnc:3829</a> which looks like
<code class="language-plaintext highlighter-rouge">10q23.3 or 10q24.2</code>. There are more sophisticated ways of represent “or” logic
in OWL, but not serializable directly in the OBO flat file format.</p>

<h3 id="ranges">Ranges</h3>

<p><a href="https://bioregistry.io/RO:0002223">RO:0002223 (starts)</a> and
<a href="https://bioregistry.io/RO:0002229">RO:0002229 (ends)</a> are used for ranges of
chromosomes, e.g., when written like <code class="language-plaintext highlighter-rouge">8q11.23-q12.1</code>, like in
<a href="https://bioregistry.io/hgnc:10263">hgnc:10263</a>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Term]
id: hgnc:10263
name: RP1
is_a: SO:0001217 ! protein_coding_gene
relationship: RO:0002223 CHR:9606-chr8q11.23 ! starts 8q11.23 (Human)
relationship: RO:0002229 CHR:9606-chr8q12.1 ! ends 8q12.1 (Human)
</code></pre></div></div>

<h3 id="special-case-mitochondria">Special case: Mitochondria</h3>

<p>Genes that are mapped to the mitochondrial chromosome get mapped to the
<a href="https://bioregistry.io">Gene Ontology (GO)</a> term
<a href="https://bioregistry.io/GO:0000262">GO:0000262</a> instead of a Chromosome Ontology
term, like in <a href="https://bioregistry.io/hgnc:50279">hgnc:50279</a>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Term]
id: hgnc:50279
name: MT-LIPCAR
is_a: SO:0002127 ! lncRNA_gene
relationship: RO:0001025 GO:0000262 ! located in mitochondrial chromosome
</code></pre></div></div>

<h3 id="qualified-annotations">Qualified Annotations</h3>

<p>Some annotations that end with a qualifier “not on reference assembly”,
“unplaced”, or “alternate reference locus” get them annotated as comment axioms.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Term]
id: hgnc:10082
name: RNR1
is_a: SO:0003001
relationship: RO:0001025 CHR:9606-chr13p12 {rdfs:comment="not on reference assembly -named gene is not annotated on the current version of the Genome Reference Consortium human reference assembly; may have been annotated on previous assembly versions or on a non-reference human assembly"} ! located in 13p12 (Human)
</code></pre></div></div>

<h3 id="unprocessable-locations">Unprocessable Locations</h3>

<p>After processing HGNC, there were several locations that could not be mapped to
CHR. I
<a href="https://github.com/monarch-initiative/monochrom/issues/34">made an issue</a> on
the Chromosome Ontology’s issue tracker noting all the locations that were not
mappable. However, several of these could be errors on the side of HGNC as well,
and requires checking each manually.</p>

<table>
  <thead>
    <tr>
      <th>unhandled location</th>
      <th>count</th>
      <th>appears in</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>10q23.3 or 10q24.2</td>
      <td>1</td>
      <td><a href="https://bioregistry.io/hgnc:3829">hgnc:3829</a></td>
    </tr>
    <tr>
      <td>Yp13.3</td>
      <td>1</td>
      <td><a href="https://bioregistry.io/hgnc:6012">hgnc:6012</a></td>
    </tr>
    <tr>
      <td>17qter</td>
      <td>1</td>
      <td><a href="https://bioregistry.io/hgnc:8841">hgnc:8841</a></td>
    </tr>
    <tr>
      <td>13cen, GRCh38 novel patch</td>
      <td>1</td>
      <td><a href="https://bioregistry.io/hgnc:15732">hgnc:15732</a></td>
    </tr>
    <tr>
      <td>Xp22.22</td>
      <td>1</td>
      <td><a href="https://bioregistry.io/hgnc:10199">hgnc:10199</a></td>
    </tr>
    <tr>
      <td>1p36.13q41</td>
      <td>1</td>
      <td><a href="https://bioregistry.io/hgnc:36026">hgnc:36026</a></td>
    </tr>
    <tr>
      <td>12q22.32</td>
      <td>1</td>
      <td><a href="https://bioregistry.io/hgnc:58534">hgnc:58534</a></td>
    </tr>
    <tr>
      <td>1q13.1</td>
      <td>1</td>
      <td><a href="https://bioregistry.io/hgnc:32558">hgnc:32558</a></td>
    </tr>
    <tr>
      <td>3q25.22</td>
      <td>1</td>
      <td><a href="https://bioregistry.io/hgnc:32563">hgnc:32563</a></td>
    </tr>
    <tr>
      <td>7p36.1</td>
      <td>1</td>
      <td><a href="https://bioregistry.io/hgnc:34871">hgnc:34871</a></td>
    </tr>
    <tr>
      <td>11p11.2</td>
      <td>1</td>
      <td><a href="https://bioregistry.io/hgnc:58650">hgnc:58650</a></td>
    </tr>
    <tr>
      <td>22pter</td>
      <td>1</td>
      <td><a href="https://bioregistry.io/hgnc:1838">hgnc:1838</a></td>
    </tr>
    <tr>
      <td>18p22.3</td>
      <td>1</td>
      <td><a href="https://bioregistry.io/hgnc:58557">hgnc:58557</a></td>
    </tr>
    <tr>
      <td>Xp11.32</td>
      <td>1</td>
      <td><a href="https://bioregistry.io/hgnc:37114">hgnc:37114</a></td>
    </tr>
    <tr>
      <td>17q12b</td>
      <td>1</td>
      <td><a href="https://bioregistry.io/hgnc:49316">hgnc:49316</a></td>
    </tr>
  </tbody>
</table>

<p>Here’s a few observations I had on this:</p>

<ul>
  <li>the <code class="language-plaintext highlighter-rouge">ter</code> seems to be an annotation related to trisomy</li>
  <li><code class="language-plaintext highlighter-rouge">13cen, GRCh38 novel patch</code> is a weird outlier</li>
  <li><code class="language-plaintext highlighter-rouge">1p36.13q41</code> might be a typo</li>
  <li>the “or” entry probably should be processed and not actually get a term, but
keeping here for completeness</li>
  <li>some of them might have typos between “p” and “q”</li>
</ul>

<p>In general, these kinds of unmapped items are not blockers towards ontologizing
a resource. It’s generally valuable to include logging in a PyOBO source when
there is content that is unhandled, since this can be valuable feedback for the
upstream resources themselves.</p>

<h3 id="membership-in-gene-groups">Membership in Gene Groups</h3>

<p>HGNC has a secondary categorization of genes into gene groups (formerly called
gene families). There’s a variety of purposes for gene groups which themselves
have a hierarchical classification. However, based on the contents of gene
groups, I don’t think that it’s appropriate to use <code class="language-plaintext highlighter-rouge">rdfs:subClassOf</code> for
relations between genes and gene sets. Instead, I have opted to use
<a href="https://bioregistry.io/RO:0002350">RO:0002350 (member of)</a>, which is defined as
a mereological relation (i.e., a part-of relation) between an item and a
collection.</p>

<h2 id="genes-and-enzymes">Genes and Enzymes</h2>

<p>HGNC annotates genes with <a href="https://bioregistry.io/ec">Enzyme Commission (EC)</a>
codes. There’s spirited discussion in the ontology world about how we should
ontologize enzymes. For example, the Gene Ontology ( GO)
<a href="https://github.com/oborel/obo-relations/issues/783#issuecomment-2706828739">models them as catalytic activities</a>
within GO’s molecular function branch.</p>

<p>We typically classify proteins based on their catalytic activities, which means
that to model the relationship between a gene and an enzyme, we need to use a
property chain connecting the gene to the protein it encodes (RO:0002205), and
the catalytic activity that the protein enables (RO:0002327).</p>

<pre><code class="language-mermaid">graph LR
    gene["AKT1 (HGNC:391)"] -- " has gene product (RO:0002205) " --&gt; protein["RAC-alpha serine/threonine-protein kinase (uniprot:P31749)"] -- " enables (RO:0002327) " --&gt; activity["non-specific serine/threonine protein kinase (EC:2.7.11.1)"]
    gene -- has gene product that enables --&gt; activity
</code></pre>

<p>This property chain doesn’t yet exist in RO, so I made a
<a href="https://github.com/oborel/obo-relations/issues/873">new term request</a> and
associated <a href="https://github.com/oborel/obo-relations/pull/874">pull request</a> to
proactively mint a new identifier for use in PyOBO. I have a pull request to
PyOBO
(<a href="https://github.com/biopragmatics/pyobo/pull/455">biopragmatics/pyobo#455</a>)
waiting to reflect this change depending on feedback. Otherwise, the old
ontologization uses a property chain of <em>gene product of</em> and <em>member of</em>, to
consider an EC class as a more general classification class.</p>

<p>I’m still undecided on what’s the best modeling choice. I am keen to fill out
the following chart in a more satisfying way, that captures a bit more nuance in
the fact that enzymes are a classification that implies the ability to carry out
an activity, but when going against the historical choices of a resource as
large as go, I am punching outside my ontological weight class 🤷.</p>

<p><img src="https://docs.google.com/drawings/d/e/2PACX-1vQkR3OSIqN3GoYJvtNtzvUWIRLYHTyciQ-PdC9OeE72gyk_kZ0cSDpZZ1dc2zgYLi4ZBexP3iZtn0mV/pub?w=1542&amp;h=1116" alt="" /></p>

<h2 id="remaining-logical-axioms-and-semantic-mappings">Remaining Logical Axioms and Semantic Mappings</h2>

<p>As I come to a close, the only remaining content to ontologize are the many
database cross-references. A first and simple approach is to use
<code class="language-plaintext highlighter-rouge">oboInOwl:hasDbXref</code>, but this is a missed opportunity to encode domain
knowledge about each of the resources. The following chart gives an overview of
the remaining logical axioms and semantic mapping types:</p>

<pre><code class="language-mermaid">graph LR
    genegroup[Gene Group&lt;br&gt;HGNC] -- " member of&lt;br&gt;(RO:0002350) " --- gene[Gene]
    geneclass[Gene Class&lt;br&gt;SO] -- is a --- gene
    gene -- " transcribed to (RO:0002511) " --&gt; rna[RNA&lt;br&gt;RNA Central, miRBase, snoRNABase]
    gene -- " has gene product&lt;br&gt;(RO:0002205) " --&gt; protein[Protein&lt;br&gt;UniProt]
    gene -- " has exact match&lt;br&gt;(skos:exactMatch) " --&gt; external1[External&lt;br&gt;NCBIGene, Ensembl, Orphanet, OMIM, RefSeq,...]
    gene -- " has database cross-reference&lt;br&gt;(oboInOwl:hasDbXref) " --&gt; externa2[External&lt;br&gt;CCDS,...]
    gene -- " is orthologous to&lt;br&gt;(RO:HOM0000017) " --&gt; orthology[Orthologous Gene&lt;br&gt;MGI, RGD]
    gene -- " has gene product that enables&lt;br&gt;(RO:0002205 + RO:0002327) " --&gt; enzyme[Enzyme&lt;br&gt;EC]
    gene -- " located in&lt;br&gt;(RO:0001025) " --&gt; chr[Chromosome Region&lt;br&gt;CHR]
</code></pre>

<p>To briefly summarize this diagram:</p>

<ol>
  <li>References to other model organism databases are modeled as orthology
relationships</li>
  <li>References to UniProt (<em>the</em> protein database) are modeled as has gene
product (i.e., a broader relationship than translation)</li>
  <li>References to RNA databases are modeled as transcription</li>
  <li>References to databases that are nomenclature resources for genes are modeled
as exact matches</li>
  <li>References to databases that can have potentially multiple experimental
measurements for a given gene are modeled with database cross-references</li>
  <li>I already mentioned in more detail above how enzymes, gene groups, and
locations are annotated.</li>
</ol>

<p>The implementation of this logic can be found in the
<a href="https://github.com/biopragmatics/pyobo/blob/main/src/pyobo/sources/hgnc/hgnc.py">PyOBO source module for HGNC</a>.</p>

<h2 id="what-was-skipped">What Was Skipped</h2>

<p>There are many extra fields in HGNC that I throw away which effectively
duplicate the HGNC identifier or gene symbol, such as <code class="language-plaintext highlighter-rouge">agr</code> (which reuses the
HGNC identifier) and <code class="language-plaintext highlighter-rouge">lncrnadb</code> (which reuses the HGNC gene symbol).</p>

<p>Rather than representing the fact that the external database provides
information about this term, a better solution is to add additional providers to
the Bioregistry, such that a given HGNC identifier can be used to create a link
to the database itself. This isn’t a perfect solution, because some databases
only cover a subset of genes. There’s more discussion about this on
<a href="https://github.com/information-artifact-ontology/ontology-metadata/issues/165">this issue</a>
on OMO issue tracker, specifically in
<a href="https://github.com/information-artifact-ontology/ontology-metadata/issues/165#issuecomment-3397905640">this comment</a>.</p>

<p>While I don’t offer an isomorphic (i.e., covers everything that’s there)
solution for ontologization of this part of the content, I do believe that the
rest of my choices address the three big benefits I mentioned at the start.</p>

<hr />

<p>Wow, this is my first ever double blog post. It took me a full day to write it,
not to mention the years of work that went into the software ecosystem itself
and the time put into improving the HGNC PyOBO source module in preparation for
writing it. I am very happy to be reporting on this, and to see how it will
positively impact the community.</p>

<p>If you made it this far and are interested in collaborating to make your own
resource accessible through PyOBO, please get in touch using my contact
information at the bottom of the page or by opening an issue on the
<a href="https://github.com/biopragmatics/pyobo/issues">PyOBO issue tracker</a>.</p>

<p>I’m also open to collaboration through grant writing or contract/consulting work
via my current employer (RWTH Aachen University) for extending and applying
PyOBO and the wider <a href="https://biopragmatics.github.io">Biopragmatics Stack</a> in
new domains. This has been previously successful in the
<a href="https://www.darpa.mil/research/programs/automating-scientific-knowledge-extraction-modeling">DARPA ASKEM</a>
and
<a href="https://www.lanl.gov/media/publications/1663/0125-ai-vaccines-drugs">DTRA RAPTER</a>
projects, and is now a key contribution to several of the DFG-funded German NFDI
consortia.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="ontology" /><category term="OWL" /><category term="genes" /><category term="HGNC" /><summary type="html"><![CDATA[This is the second of a two-part post about encoding databases as ontologies. In the first part, I gave a background on how I started working on this problem and the software stack I developed along the way. In this post, I explain the philosophy and design about how I encoded the HGNC (HUGO Gene Nomenclature Committee) database as an ontology using PyOBO.]]></summary></entry><entry><title type="html">Bridging NFDI’s culture and chemistry knowledge graphs</title><link href="https://cthoyt.com/2025/10/07/bridging-culture-and-chemistry.html" rel="alternate" type="text/html" title="Bridging NFDI’s culture and chemistry knowledge graphs" /><published>2025-10-07T21:18:00+00:00</published><updated>2025-10-07T21:18:00+00:00</updated><id>https://cthoyt.com/2025/10/07/bridging-culture-and-chemistry</id><content type="html" xml:base="https://cthoyt.com/2025/10/07/bridging-culture-and-chemistry.html"><![CDATA[<p>At the sixth NFDI4Chem consortium meeting,
<a href="https://www.adwmainz.de/personen/mitarbeiterinnen/profil/torsten-schrade.html">Torsten Schrade</a>
from the NFDI4Culture consortium gave a lovely and whimsical talk entitled
<a href="https://zenodo.org/records/17127336"><em>A Data Alchemist’s Journey through NFDI</em></a>
which explored ways that we might federate and jointly query both consortia’s
knowledge via their respective SPARQL endpoints. He proposed a toy example in
which he linked paintings depicting alchemists trying to make gold to compounds
containing gold. This post is about the steps I took to automate his toy example
and extend it to not only chemicals or compounds represented in Iconclass, but
also equipment and devices.</p>

<h2 id="part-1-the-semantic-lay-of-the-land">Part 1: The Semantic Lay of the Land</h2>

<p>The first step in effectively integrating data and writing federated SPARQL
queries is to understand the landscape ontologies, controlled vocabularies
(CVs), and databases whose entities will be referenced in SPARQL queries and
will appear in the knowledge graph(s). It also requires an understanding of the
syntaxes used to reference these entities, namely the uniform resource
identifier (URI) syntax (e.g., <code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/CHMO_0000073</code>) or
compact uniform resource identifier (CURIE) syntax (e.g., <code class="language-plaintext highlighter-rouge">CHMO:0000073</code>). For
more information on URIs and CURIEs, see <a href="/2021/09/14/curies.html">my previous
post</a>.</p>

<p>For example, the following SPARQL query against the NFDI4Chem knowledge graph
uses SPARQL’s <code class="language-plaintext highlighter-rouge">PREFIX</code> syntax to abbreviate long URIs and write a compact query.</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">PREFIX</span><span class="w"> </span><span class="nn">prov</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/ns/prov#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">rdf</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/1999/02/22-rdf-syntax-ns#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">CHMO</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://purl.obolibrary.org/obo/CHMO_&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="err">nfdi4chem</span><span class="p">.</span><span class="nn">doi</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;https://doi.org/10.14272/&gt;</span><span class="w">

</span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nv">?dataset</span><span class="w"> </span><span class="nn">prov</span><span class="o">:</span><span class="ss">wasGeneratedBy</span><span class="o">/</span><span class="nn">prov</span><span class="o">:</span><span class="ss">used</span><span class="w"> </span><span class="nv">?experiment</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="nv">?experiment</span><span class="w"> </span><span class="nn">prov</span><span class="o">:</span><span class="ss">wasGeneratedBy</span><span class="o">/</span><span class="nn">rdf</span><span class="o">:</span><span class="ss">type</span><span class="w"> </span><span class="nn">CHMO</span><span class="o">:</span><span class="ss">0000073</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">4</span><span class="w">
</span></code></pre></div></div>

<p>When executed, this query returns the experiments containing an artifact
generated by scanning electron microscopy (<code class="language-plaintext highlighter-rouge">CHMO:0000073</code>) and the datasets that
they are part of:</p>

<table>
  <thead>
    <tr>
      <th>dataset</th>
      <th>experiment</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://doi.org/10.14272/YJAIWVDPLYJOFU-UHFFFAOYSA-B/CHMO0000073">nfdi4chem.doi:YJAIWVDPLYJOFU-UHFFFAOYSA-B/CHMO0000073</a></td>
      <td>nfdi4chem.doi:YJAIWVDPLYJOFU-UHFFFAOYSA-B/CHMO0000073/spectrum</td>
    </tr>
    <tr>
      <td><a href="https://doi.org/10.14272/YJAIWVDPLYJOFU-UHFFFAOYSA-B/CHMO0000073">nfdi4chem.doi:YJAIWVDPLYJOFU-UHFFFAOYSA-B/CHMO0000073</a></td>
      <td>nfdi4chem.doi:YJAIWVDPLYJOFU-UHFFFAOYSA-B/CHMO0000073/spectrum</td>
    </tr>
    <tr>
      <td><a href="https://doi.org/10.14272/ABCUGETYYULVMM-UHFFFAOYSA-N/CHMO0000073">nfdi4chem.doi:ABCUGETYYULVMM-UHFFFAOYSA-N/CHMO0000073</a></td>
      <td>nfdi4chem.doi:ABCUGETYYULVMM-UHFFFAOYSA-N/CHMO0000073/spectrum</td>
    </tr>
    <tr>
      <td><a href="https://doi.org/10.14272/ABCUGETYYULVMM-UHFFFAOYSA-N/CHMO0000073">nfdi4chem.doi:ABCUGETYYULVMM-UHFFFAOYSA-N/CHMO0000073</a></td>
      <td>nfdi4chem.doi:ABCUGETYYULVMM-UHFFFAOYSA-N/CHMO0000073/spectrum</td>
    </tr>
  </tbody>
</table>

<p>This immediately leads to a few questions:</p>

<ul>
  <li>What are <code class="language-plaintext highlighter-rouge">prov</code>, <code class="language-plaintext highlighter-rouge">rdf</code>, <code class="language-plaintext highlighter-rouge">CHMO</code> and <code class="language-plaintext highlighter-rouge">nfdi4chem.doi</code> that appear with in
<code class="language-plaintext highlighter-rouge">PREFIX</code> lines of the SPARQL query and later in the results?</li>
  <li>Where do they come from?</li>
  <li>How do I know the correct URI prefix to use with the CURIE prefix (e.g.,
<code class="language-plaintext highlighter-rouge">http://www.w3.org/ns/prov#</code> goes with <code class="language-plaintext highlighter-rouge">prov</code>)?</li>
  <li>How do I know which prefixes are relevant in my domain (e.g., chemistry)?</li>
  <li>How do I know which prefixes to use when writing a SPARQL query? (I’m going to
gloss over this question, since the best answer is usually for the knowledge
graph maintainers to write more/better documentation)</li>
</ul>

<p>Besides the last question, the answer is the
<a href="https://bioregistry.io">Bioregistry</a>: an open source, community-curated
database of prefixes for ontologies, CVs, databases, and other resources that
mint identifiers that might appear in SPARQL queries or in knowledge graphs. In
the next few sections of this post, I’ll explain how the Bioregistry answers
these questions.</p>

<p>Caveat: despite its name, the Bioregistry is cross-disciplinary, and we’re
currently working on rebranding it to better reflect this.</p>

<h3 id="resolving-prefixes-curies-and-uris-with-the-bioregistry">Resolving prefixes, CURIEs, and URIs with the Bioregistry</h3>

<p>The Bioregistry can be used interactively from its web interface to search for
prefixes either quickly from <a href="https://bioregistry.io">the homepage</a> or in more
detail from the <a href="https://bioregistry.io/registry">catalog page</a>.</p>

<p>If you already know the prefix, you can construct a URL by adding it to the end
of <code class="language-plaintext highlighter-rouge">https://bioregistry.io</code> like
<a href="https://bioregistry.io/chmo">https://bioregistry.io/CHMO</a>. These lead to pages
that describe the resource, say what is the URI prefix that should appear in
your SPARQL queries. Alternatively, entries in the Bioregistry have textual
descriptions, keywords, and tags that can be searched to find ontologies, CVs,
and databases relevant for your domain.</p>

<p>If you have a CURIE, you can use the Bioregistry to automatically link to a
webpage by adding it to the end of <code class="language-plaintext highlighter-rouge">https://bioregistry.io</code> like
<a href="https://bioregistry.io/CHMO:0000073">https://bioregistry.io/CHMO:0000073</a>.</p>

<h3 id="extending-the-bioregistry">Extending the Bioregistry</h3>

<p>When a prefix is missing from the Bioregistry, anyone can make a suggestion to
add a new one, even if it’s not for your resource
(<a href="https://www.youtube.com/watch?v=e-I6rcV2_BE&amp;">video tutorial</a>). When
maintainers suggest a prefix for their own resource, it’s also a valuable
opportunity to get feedback on if the prefix and URI schema
<a href="https://doi.org/10.1371/journal.pbio.2001414">follow best practices</a>.</p>

<p><code class="language-plaintext highlighter-rouge">rdfs</code>, <code class="language-plaintext highlighter-rouge">prov</code>, and <code class="language-plaintext highlighter-rouge">CHMO</code> are already in the Bioregistry, but there’s nothing
that reflects <code class="language-plaintext highlighter-rouge">nfdi4chem.doi</code>. This means there’s an opportunity here to add a
new prefix! I already chatted with the Chemotion team about this at the
NFDI4Chem meeting and am keen to help them adopt
<a href="https://doi.org/10.1371/journal.pbio.2001414">best practices in minting identifiers</a>,
then get their resources registered in the Bioregistry.</p>

<h2 id="part-2-operationalization-of-resources">Part 2: Operationalization of Resources</h2>

<p>After we have found the relevant prefixes, CURIEs, and URIs using the
Bioregistry, we also want to make sure that we can download the associated
ontologies, CVs, or databases. For ontologies, there are explicit fields in the
Bioregistry to link to OWL, OBO, or SKOS artifacts (when available).</p>

<p>However, for databases, each typically needs its own custom adapter into an
ontology-like format. This section focuses on doing this for Iconclass.</p>

<h3 id="iconclass">Iconclass</h3>

<p><a href="https://iconclass.org">Iconclass</a> is a controlled vocabulary used to annotate
parts of images with what they depict. For example,
<a href="https://bioregistry.io/iconclass:49E3911"><code class="language-plaintext highlighter-rouge">iconclass:49E3911</code></a> is used to
annotate a part of an image depicting an <em>alchemist trying to make gold</em>.
Iconclass identifiers implicitly contain the hierarchy:</p>

<ul>
  <li><a href="https://bioregistry.io/iconclass:49E391"><code class="language-plaintext highlighter-rouge">iconclass:49E391</code></a> is used to
annotate an <em>alchemist at work</em></li>
  <li><a href="https://bioregistry.io/iconclass:49E39"><code class="language-plaintext highlighter-rouge">iconclass:49E39</code></a> is used to
annotate <em>alchemy</em></li>
  <li><a href="https://bioregistry.io/iconclass:49E3"><code class="language-plaintext highlighter-rouge">iconclass:49E3</code></a> is used to annotate
<em>chemistry</em></li>
  <li><a href="https://bioregistry.io/iconclass:49E"><code class="language-plaintext highlighter-rouge">iconclass:49E</code></a> is used to annotate
<em>science and technology</em></li>
  <li><a href="https://bioregistry.io/iconclass:49"><code class="language-plaintext highlighter-rouge">iconclass:49</code></a> is used to annotate
<em>education, science, and learning</em></li>
  <li><a href="https://bioregistry.io/iconclass:4"><code class="language-plaintext highlighter-rouge">iconclass:4</code></a> is used to annotate
<em>Society, Civilization, Culture</em></li>
</ul>

<p>Here’s how its web browser looks:</p>

<p><a href="https://bioregistry.io/iconclass:49E391"><img src="/img/iconclass-alchemist.png" alt="" /></a></p>

<p>Iconclass doesn’t appear to be curated like an ontology, so the hierarchy is
often confusing to follow. I found it easier to understand the logic behind the
hierarchy if you prepend “depiction of” or “depiction of something related to”
in front of each label. This also makes up for some of the illogical
hierarchical relations. Here’s how I imagine the same hierarchy could be
clarified with better labels:</p>

<ul>
  <li><a href="https://bioregistry.io/iconclass:49E391"><code class="language-plaintext highlighter-rouge">iconclass:49E391</code></a> <em>depiction of an
alchemist at work</em></li>
  <li><a href="https://bioregistry.io/iconclass:49E39"><code class="language-plaintext highlighter-rouge">iconclass:49E39</code></a> <em>depiction of
alchemy</em></li>
  <li><a href="https://bioregistry.io/iconclass:49E3"><code class="language-plaintext highlighter-rouge">iconclass:49E3</code></a> <em>depiction of
something related to chemistry</em></li>
  <li><a href="https://bioregistry.io/iconclass:49E"><code class="language-plaintext highlighter-rouge">iconclass:49E</code></a> <em>depiction of
something related to science or technology</em></li>
  <li><a href="https://bioregistry.io/iconclass:49"><code class="language-plaintext highlighter-rouge">iconclass:49</code></a> <em>depiction of something
related to education, science, or learning</em></li>
  <li><a href="https://bioregistry.io/iconclass:4"><code class="language-plaintext highlighter-rouge">iconclass:4</code></a> <em>depiction of something
related to society, civilization, or culture</em></li>
</ul>

<p>Because Iconclass isn’t curated as an ontology, there isn’t an OWL or OBO file
that can be used with standard tooling. However, some of the source data is
available on GitHub at <a href="https://github.com/iconclass/data">iconclass/data</a>, so
it’s possible to write custom code that wrangles it into an ontology-like shape.
I’ve actually done this for dozens of repositories already, and written
<a href="https://github.com/biopragmatics/pyobo"> PyOBO</a>, a library of reusable tooling
to support ingesting new resources across domains in an ontology-like shape.</p>

<p>Accordingly, I added a source to PyOBO to ingest Iconclass in
<a href="https://github.com/biopragmatics/pyobo/pull/433">biopragmatics/pyobo#433</a>. This
not only enables it to generate ontology-like artifacts in the OWL and OBO
formats, but also gives access to the
<a href="https://academic.oup.com/bioinformatics/article/39/4/btad130/7077133">text mining and FAIR mapping tools built on top of PyOBO</a>.</p>

<p>Along the way, I found that Iconclass has a lot more weird and irregular
identifiers than I had earlier assumed. I was able to make an additional pull
request to the Bioregistry in
<a href="https://github.com/biopragmatics/bioregistry/pull/1686">biopragmatics/bioregistry#1686</a>
to update the underlying regular expression pattern and add extra examples to
demonstrate the weirdness. This is important because PyOBO uses the Bioregistry
for regular expression validation of identifiers internally, and without this
update, the Iconclass source doesn’t work!</p>

<h2 id="part-3-bridging-the-semantic-gap">Part 3: Bridging the Semantic Gap</h2>

<p>The next goal was to identify entries in Iconclass correspond to elements,
compounds, laboratory equipment, or other terms relevant in the chemistry
domain, and create semantic mappings that can serve as a “semantic bridge”
between disciplines.</p>

<p>This makes use of
<a href="https://mapping-commons.github.io/sssom/">Simple Standard for Sharing Ontological Mappings (SSSOM)</a>
as a community standard for storing semantic mappings and giving access to
standardized tooling for accessing, querying, and applying them.</p>

<h3 id="first-attempt-lexical-matching">First attempt: lexical matching</h3>

<p>The <a href="https://github.com/biopragmatics/biomappings">Biomappings</a> project provides
tools for predicting semantic mappings using lexical matching in SSSOM. Much
like the Bioregistry, this has a bit of a nomenclature issue and is actually
cross-disciplinary. Anyway, it can quickly be used to spin up a workflow for
matching any two vocabularies available through PyOBO with a few lines. I gave
it a try to match Iconclass to the
<a href="https://bioregistry.io/chmo">Chemical Methods Ontology (CHMO)</a>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">biomappings.lexical</span> <span class="kn">import</span> <span class="n">lexical_prediction_cli</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">lexical_prediction_cli</span><span class="p">(</span><span class="n">__file__</span><span class="p">,</span> <span class="s">"iconclass"</span><span class="p">,</span> <span class="s">"chmo"</span><span class="p">)</span>
</code></pre></div></div>

<p>This usually works well for matching entities in resources curated as
ontologies, but because Iconclass’s labels aren’t typical, it wasn’t able to
generate more than a handful of matches.</p>

<h3 id="second-attempt-language-models-and-embedding-similarity">Second attempt: language models and embedding similarity</h3>

<p>This prompted me to take a different approach that relies on (medium) language
models to generate embeddings, which are better able to capture the subtle
differences in the way entities are labeled. This led to the following
improvements:</p>

<ol>
  <li>I added functionality to PyOBO to get a dataframe of embeddings for <em>all</em>
entities in a given ontology or controlled vocabulary in
<a href="https://github.com/biopragmatics/pyobo/pull/434">biopragmatics/pyobo#434</a></li>
  <li>I extended the lexical prediction workflow in Biomappings to have a method
that combines embedding generation in PyOBO with similarity calculation and
finally the application of a similarity cutoff for calling mappings in
<a href="https://github.com/biopragmatics/biomappings/pull/206">biopragmatics/biomappings#206</a>.</li>
</ol>

<p>After this, I was able to update my workflow to look like this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">biomappings.lexical</span> <span class="kn">import</span> <span class="n">lexical_prediction_cli</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">lexical_prediction_cli</span><span class="p">(</span>
        <span class="n">__file__</span><span class="p">,</span>
        <span class="s">"iconclass"</span><span class="p">,</span>
        <span class="s">"chmo"</span><span class="p">,</span>
        <span class="n">method</span><span class="o">=</span><span class="s">"embedding"</span><span class="p">,</span>
        <span class="n">cutoff</span><span class="o">=</span><span class="mf">0.9</span>
    <span class="p">)</span>
</code></pre></div></div>

<h3 id="third-attempt-ner">Third Attempt: NER</h3>

<p>Embedding similarity worked well enough for mapping Iconclass records to CHMO
and the
<a href="https://bioregistry.io/registry/obi">Ontology for Biomedical investigations (OBI)</a>
(another ontology containing experimental equipment), but it didn’t work at all
for ChEBI because Iconclass records that mention chemicals typically have a
large amount of other text.</p>

<p>This led me to reformulate mapping as a named entity recognition (NER) task, for
which I implemented yet another new workflow in Biomappings:
<a href="https://github.com/biopragmatics/biomappings/pull/209">biopragmatics/biomappings#209</a></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">biomappings.lexical</span> <span class="kn">import</span> <span class="n">lexical_prediction_cli</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">lexical_prediction_cli</span><span class="p">(</span>
        <span class="n">__file__</span><span class="p">,</span>
        <span class="s">"iconclass"</span><span class="p">,</span>
        <span class="s">"chebi"</span><span class="p">,</span>
        <span class="n">method</span><span class="o">=</span><span class="s">"ner"</span><span class="p">,</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>After all of this, I added a first set of curations to the Biomappings project
in
<a href="https://github.com/biopragmatics/biomappings/pull/205">biopragmatics/biomappings#205</a>
which are stored in SSSOM within the GitHub repository. Normally, I commit all
predictions, but they are so noisy and numerous, that I only committed the
curations since I focused on a small set that will support the bigger story
here, and larger scale curation can be done for Iconclass to chemistry (and
other domains) in a follow-up.</p>

<h2 id="interlude-1-exploring-new-sparql-endpoints">Interlude 1: Exploring New SPARQL Endpoints</h2>

<p>Before diving in fully federated queries across multiple sources, I want to warm
up by making self-contained queries over the respective NFDI4Culture and
NFDI4Chem knowledge graphs. Unfortunately, neither of them are well documented
for what I want to do, so put on your pith helmet and get ready for some digital
archaeology.</p>

<h3 id="querying-nfdi4culture">Querying NFDI4Culture</h3>

<p>The NFDI4Culture Consortium makes its knowledge graph queryable from SPARQL
here:
<a href="https://nfdi4culture.de/resources/knowledge-graph.html">https://nfdi4culture.de/resources/knowledge-graph.html</a>.
Using the <a href="https://nfdi4culture.de/go/kg-query-iconclass-chemistry">example</a>
from Thorsten’s talk as a starting point, and I wrote the following SPARQL query
for Iconclasses, objects they are annotated to, and URLs for digital depictions
of those objects.</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">PREFIX</span><span class="w"> </span><span class="nn">cto</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;https://nfdi4culture.de/ontology#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">iconclass</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;https://iconclass.org/&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">rdfs</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/2000/01/rdf-schema#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">schema</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://schema.org/image&gt;</span><span class="w">

</span><span class="k">SELECT</span><span class="w"> </span><span class="nv">?resource</span><span class="w"> </span><span class="nv">?resourceLabel</span><span class="w"> </span><span class="nv">?imageURL</span><span class="w"> </span><span class="nv">?iconclass</span><span class="w">
</span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nv">?resource</span><span class="w"> </span><span class="nn">cto</span><span class="o">:</span><span class="ss">subjectConcept</span><span class="w"> </span><span class="nv">?iconclass</span><span class="w"> </span><span class="p">;</span><span class="w">
            </span><span class="nn">schema</span><span class="o">:</span><span class="ss">image</span><span class="w"> </span><span class="nv">?imageURL</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">FILTER</span><span class="w"> </span><span class="nb">STRSTARTS</span><span class="p">(</span><span class="nb">STR</span><span class="p">(</span><span class="nv">?iconclass</span><span class="p">),</span><span class="w"> </span><span class="nb">STR</span><span class="p">(</span><span class="nn">iconclass</span><span class="o">:</span><span class="p">))</span><span class="w">
  </span><span class="k">OPTIONAL</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">?resource</span><span class="w"> </span><span class="nn">rdfs</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="nv">?resourceLabel</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">50</span><span class="w">
</span></code></pre></div></div>

<p>One record that was returned was
<a href="https://www.bildindex.de/document/obj00003367">Bildnis Professor Hoffmann (bildindex:obj00003367)</a>
from the <a href="https://www.bildindex.de">BildIndex der Kunst &amp; Architektur</a>. Notably,
this image depicts the eponymous professor with his
<a href="https://bioregistry.io/iconclass:49E2512">microscope (iconclass:49E2512)</a>.
BildIndex didn’t give enough context for me to figure out who Professor Hoffman
was (e.g., by connecting to Wikidata or other external resources that describe
notable people), but it did say that the painting is in Bonn! Maybe I will go
track it down in person to bring this blog post full circle.</p>

<p><img src="https://www.bildindex.de/bilder/m/fmla915_09" alt="" /></p>

<h4 id="minor-criticisms">Minor Criticisms</h4>

<p>I immediately identified two issues with the NFDI4Culture knowledge graph:
first, there are no English labels for resources. This is understandable given
this is a German project and unlike in science, there isn’t a huge pressure for
internationalization, but it does reduce the value of the resource for anyone
outside the Germanophone world. Second, even worse, there are no labels for
Iconclass in German, English, nor any other language. Because I produced
ontology artifacts for Iconclass (as described earlier in this post), I can get
these labels in RDF by federating over my own OWL files, but I think that this
would be an important addition to the NFDI4Culture knowledge graph to improve
its own usability.</p>

<h3 id="querying-nfdi4chem">Querying NFDI4Chem</h3>

<p>Time to switch domains! The NFDI4Chem Consortium makes its knowledge graph
queryable from SPARQL here:
<a href="https://search.nfdi4chem.de/sparql">https://search.nfdi4chem.de/sparql</a>. I
started with the following SPARQL query to investigate which measurement
processes from CHMO appear like NMR, mass spectrometry, X-ray diffraction, and
microscopy.</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">PREFIX</span><span class="w"> </span><span class="nn">schema</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://schema.org/&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">rdfs</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/2000/01/rdf-schema#&gt;</span><span class="w">

</span><span class="k">SELECT</span><span class="w"> </span><span class="nv">?o</span><span class="w"> </span><span class="nv">?label</span><span class="w"> </span><span class="p">(</span><span class="nb">COUNT</span><span class="p">(</span><span class="nv">?o</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nv">?count</span><span class="p">)</span><span class="w">
</span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nv">?s</span><span class="w"> </span><span class="nv">?p</span><span class="w"> </span><span class="nv">?o</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">OPTIONAL</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">?o</span><span class="w"> </span><span class="nn">schema</span><span class="o">:</span><span class="ss">name</span><span class="w"> </span><span class="nv">?label</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="k">FILTER</span><span class="p">(</span><span class="nb">STRSTARTS</span><span class="p">(</span><span class="nb">STR</span><span class="p">(</span><span class="nv">?o</span><span class="p">),</span><span class="w"> </span><span class="s2">"http://purl.obolibrary.org/obo/CHMO_"</span><span class="p">))</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="nv">?o</span><span class="w"> </span><span class="nv">?label</span><span class="w">
</span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">DESC</span><span class="p">(</span><span class="nv">?count</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>While most of the techniques currently appearing in the NFDI4Chem knowledge
graph are a bit too modern to be of (wide) interest in the cultural heritage
world, the term for
<a href="http://purl.obolibrary.org/obo/CHMO_0000073">scanning electron microscopy (CHMO:0000073)</a>
is a descendant of
<a href="http://purl.obolibrary.org/obo/CHMO_0000067">microscopy (CHMO:0000067)</a>, and
this is something that has a corresponding Iconclass (49E2512) for the depiction
of a microscope which I curated earlier using Biomappings.</p>

<h4 id="on-the-impedance-between-processes-and-material-entities">On the Impedance between Processes and Material Entities</h4>

<p>You’d be correct in saying that I made a bit of a hop from the physical
instrument of a
<a href="http://purl.obolibrary.org/obo/CHMO_0000953">microscope (CHMO:0000953)</a> to the
process of
<a href="http://purl.obolibrary.org/obo/CHMO_0000067">microscopy (CHMO:0000067)</a>. Please
give me a bit of wiggle room, since it’s already hard enough to thread the
needle between culture and chemistry. Luckily, CHMO uses a logical axiom on the
definition of microscopy to denote that all microscopy has a microscope as a
<a href="http://purl.obolibrary.org/obo/BFO_0000057">participant (BFO:0000057)</a>.</p>

<p>While we’ll need this axiom later to construct a more sophisticated SPARQL query
that can resolve the impedance between the sense that I curated in the mappings
and what appears in experimental data, it doesn’t appear that the NFDI4Chem
knowledge graph imports a reasoned version of CHMO that materializes this axiom
as a triple <code class="language-plaintext highlighter-rouge">&lt;CHMO:0000067, BFO:0000057, CHMO:0000953&gt;</code>. This means that we’ll
have to inject this triple ourselves via federation later.</p>

<p>It’s easy to get confused in the semantic web, ontologies, and RDF world.
Ontologies are typically encoded using the OWL schema, which can be serialized
to XML and RDF. However, this doesn’t mean that it is RDF that represents the
graph you might expect an ontology induces. For example, the existential
restriction that all microscopy has a microscope as a participant is encoded in
OWL like this:</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">CHMO:</span><span class="mi">0000067</span><span class="w"> </span><span class="nn">rdf:</span><span class="n">type</span><span class="w"> </span><span class="nn">owl:</span><span class="n">Class</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">rdfs:</span><span class="n">subClassOf</span><span class="w"> </span><span class="nn">OBI:</span><span class="mi">0000185</span><span class="w"> </span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="w"> </span><span class="nn">rdf:</span><span class="n">type</span><span class="w"> </span><span class="nn">owl:</span><span class="n">Restriction</span><span class="w"> </span><span class="p">;</span><span class="w">
          </span><span class="nn">owl:</span><span class="n">onProperty</span><span class="w"> </span><span class="nn">BFO:</span><span class="mi">0000057</span><span class="w"> </span><span class="p">;</span><span class="w">
          </span><span class="nn">owl:</span><span class="n">someValuesFrom</span><span class="w"> </span><span class="nn">CHMO:</span><span class="mi">0000953</span><span class="w"> </span><span class="p">.</span><span class="w">
        </span><span class="p">]</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>This means if you want to get the aforementioned triple
<code class="language-plaintext highlighter-rouge">&lt;CHMO:0000067, BFO:0000057, CHMO:0000953&gt;</code>, you’ll need the following SPARQL:</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="nv">?s</span><span class="w"> </span><span class="nv">?p</span><span class="w"> </span><span class="nv">?o</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">?s</span><span class="w"> </span><span class="nn">rdfs</span><span class="o">:</span><span class="ss">subClassOf</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="nn">rdf</span><span class="o">:</span><span class="ss">type</span><span class="w"> </span><span class="nn">owl</span><span class="o">:</span><span class="ss">Restriction</span><span class="p">;</span><span class="w">
        </span><span class="nn">owl</span><span class="o">:</span><span class="ss">onProperty</span><span class="w"> </span><span class="nv">?p</span><span class="w"> </span><span class="p">;</span><span class="w">
        </span><span class="nn">owl</span><span class="o">:</span><span class="ss">someValuesFrom</span><span class="w"> </span><span class="nv">?o</span><span class="w">
    </span><span class="p">]</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The NFDI4Chem knowledge graph doesn’t actually import CHMO (nor any other
ontologies at the moment) so in order to incorporate this into a larger query,
we’d have to federate once again!</p>

<h4 id="exploring-microscopy-in-nfdi4chem">Exploring Microscopy in NFDI4Chem</h4>

<p>After a bit of exploring, I was able to construct a query (which you may
remember from earlier in this post) that returns all experimental results
(<code class="language-plaintext highlighter-rouge">?experiment</code>) that were generated by microscopy, and the dataset/record where
they were recorded.</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">PREFIX</span><span class="w"> </span><span class="nn">prov</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/ns/prov#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">rdf</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/1999/02/22-rdf-syntax-ns#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">CHMO</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://purl.obolibrary.org/obo/CHMO_&gt;</span><span class="w">

</span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nv">?dataset</span><span class="w"> </span><span class="nn">prov</span><span class="o">:</span><span class="ss">wasGeneratedBy</span><span class="o">/</span><span class="nn">prov</span><span class="o">:</span><span class="ss">used</span><span class="w"> </span><span class="nv">?experiment</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="nv">?experiment</span><span class="w"> </span><span class="nn">prov</span><span class="o">:</span><span class="ss">wasGeneratedBy</span><span class="o">/</span><span class="nn">rdf</span><span class="o">:</span><span class="ss">type</span><span class="w"> </span><span class="nn">CHMO</span><span class="o">:</span><span class="ss">0000073</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>One of the twelve records (as of September 18<sup>th</sup>, 2025) returned by
this query was a scanning electron microscopy (SEM) dataset performed on a
zirconium-containing inorganic molecule. The URI for the experiment,
<code class="language-plaintext highlighter-rouge">https://doi.org/10.14272/YJAIWVDPLYJOFU-UHFFFAOYSA-B/CHMO0000073/spectrum</code>,
does not resolve, but the dataset at
<a href="https://doi.org/10.14272/YJAIWVDPLYJOFU-UHFFFAOYSA-B/CHMO0000073">https://doi.org/10.14272/YJAIWVDPLYJOFU-UHFFFAOYSA-B/CHMO0000073</a>
does. These URIs make me want to cry - they do not follow any best practices for
how identifiers should look as prescribed by Julie McMurry <em>et al.</em> (2017) in
<a href="https://doi.org/10.1371/journal.pbio.2001414">Identifiers for the 21<sup>st</sup> century</a>.
Despite my grumblings, they can still be used as URIs and function well enough
for SPARQL queries.</p>

<p>Ultimately, here’s the image from the SEM in the experiment in question, though
I wasn’t able to construct a SPARQL query that returned it:</p>

<p><img src="data:image/png;base64,/9j/4AAQSkZJRgABAQIASwBLAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCACAAIABAREA/8QAHAAAAgMBAQEBAAAAAAAAAAAAAAQCAwUBBgcI/8QAOBAAAgEDAwIFAgQDBwUAAAAAAQIDAAQRBRIhMVEGEyJBYRRxMoGh0RWxwSMkQpGT4fAHUlWy8f/aAAgBAQAAPwD7/RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRXxkf9R9SuIQhuHiuGIVduNv55HWtuy8Razq2lyx211JHcQgsspC5kwMkYPHbpScureLbO2FzfXc8EHHrKoevTgVlnxl4gLHbqcpBPp9C/tWjY+I9XlQTah4ga0gY+j0KzNz7DFX3viDWSHNpqcytEMyKwVhgnhgQOnSuxeLbm9hkkTWpLaSNcbXQFHIHJHBOehrHbxprygn+Jy49vSv7U+PGGv6bHHHfXLGdn3AFVPox7469fypC/wDHmsT6lDDZXs8UTAB8hD6u446U43iXxEkHkfxhfNR8MSq7z198YxVNj4v164ZidTYqnJ3bR/SoXHi7xAZZZo9WYQF/QoCHC574pufxnrLTSLHeOiKBwu1mx3yB1pMeNdfa4iiS/JilBWNyV3ZI4LDFXaL4w1+68Q/STaluSKZUkidQpbnHGB0rxn05dlAALEgAYzW1azJZPE8k0XlIhVY2BGMZJORULXUJp7ScOr3Bdg4yu4gDpx2q3TNaGi3FvMscZnmkwx2hsLjoB7VXqeq3XiAyQMzOySkkOeFXJOR8Y7V3TL3znmCSytHAhSVXIUfYZ605dSC6txPbbFeSJSiSKEwQcZGevHX71Q9pZNayRGffdRjc8jjABA5UAdRWXNNK+1ycS9CSARj/AJikGgLbQpOSw6da1Jr2EyMsMRRRGUJc5IY9T811J9loJ3QylVPoXPqP/aKyrqVbyVGRDAjKWKElsEfrWxpcrO00Vs0gkiO+WRc8nHGe3+9RR/qr1b2GHKjDeTnay4HPAru2S38aaZKIiiXlwrAgY6MPSSewqdnY3zQPepZSNbRqcyspCgfBrtqwk0szpFHKYCC8YUH3+evFUyaklqblbWGNLlZcI4B4Q9R/Kkje3kkm8GMSucltnt2H5ZpYs0moF4X8pnGNwzggDmvSRyRzi3LuluhAEjRL6mOOMk/rWPfPLqDxyLO0qxnamW5UA56Vp6UYZYVkluma5UsSjDjjoCc9DUL2JY7h9q7Yyx2jdnFV21pNNHJPBHu8vv078/lWeE83ybhlUAlsEHH5/an/AA3NdwXc6sVRHYlTKpZMY6rkc+/HzXoZTbtYzfQWlnNeF8LKCFyCeuDggj71nXjrpM5ktoHSZX2zmVgyv2PHTHeqDMt7fSXgBt5JE2yJHgbxjBYEdCeODT2heXHcWsd5N9fNBMgtlDY2+vJJ/wA6mmuX4ie0t5Glt4lCASkbSpJyD80hdzE6E0lujQuZQpKr1HOTn8qxwikbjkk8kn3owSwIAyp4+KiFLiUF1GOhPJHfFWRTCOzEoBkmHpUZ6juR39quFjubdZqvlqMsryD0k/rUIwbe7jmwgQPl2bkAfPenLSaGB41ZVeHcQyyMfUDwOacgvI7eCaOykMUZkdfKLF8MOMke4P8ASkb27aWI2zi3EuBIWjjA2Dodo9xSV7dKs5WCQRXEa7o0duOmeasja6ulW8UDdEFLKGx6u2D160prkuq6rbtfWc8KCKMLJbSHlmBxlSPfn3qGkTXh09PqkRJpMAMeCB75r0vhmSG+1yLzJWMYlQRlWxtIPyOckVmN9Pbq2x3heMhpgDnePkH4zWiVm1HSfqbeLbbRnhRgEjn1EdayhGCSFNBXHHX7e9W2gSKc+YIj5i4BZd23P9alb2lijyC8lZTIpAEC5wfY84xRdWttCDCssUsMRU/2bEHJ5OTxzVc8M1o4LIymTLK2ziRDxSzx7oZQqERg9Ceo7ikPLnhZBAB5pPGByeeKa1N5rEx7lPqjH9qvqCMeoPb7UhcW/wBRbmSAmOSQ8NjceO9Rh1C6VorHHnvkKx3YGe9MXUV/EsJlbIYH8P4c/l1poXkttLGLeQiUIX3Mnpyen51f4cinj1qxkjYCATpujI4yXAJNbLaRZSK96Ip1nJAZA+0N2z7HvWUHudOvhbSMilmxuTleffP581pyX/o2oiG5VQC0iYyucZz7mrJbsaXbmxa3ZDLJvYA5A7errjBFYkkltJKyqrRklQUJOB8g1N0RTNuheUO5Cyb8AKewFKxeVDetNDbPIc5LEk/lTl1fm7kHmB82qn178bgfYD4xVVuitdRSMCqOoEivlgvycU7po0sakxkcL5StkgEktjjHP51VqV3b205SCcmCT1OjDcWI6H496yYfqmvIWaLzUdshwPSq/Ncu7SePUpHkW2cMQwaPjjtnvWrDHYXcqWkSzeZA3pYyEhyw5BHSk7u6CXbqIJE2nALjr1wRWnp0EaazpskTMwaZNxHAzuB6Uvb3U0d+AbvMDBfMEcZ7+xrW+l0+JY7xInIRyjK8mdox+P5+1YM6TGQyIz54VcHIP2HenFinMJ86dSQnl+YzeoN7fypRw6ypZxgTBpMOUQksa1YrG11HIgtlSUhkRHlKrv8AjP8AWs+300rbbllRnhkYMFJH3HbqKLe3Yagt0YQFTLlWGVJ6cd+aVuhJFFM8LIyO+WTrx2x9q5HIl0CIkYHbyvTk8DP+VWPp0UNkqyHDwg8g/jyf55p1Ll9PsI5UtfSzFSJSCVHsPzFZMcyPqMhkCrDIckY4XHzTodYJyUj3OfwvgAP2OfzqdzqF0kCXVzCqqwyyA45HGRjrW1oCrcahaSLnzDNGYw/A25yQfmsc+XFaJhAMMrMxPuR+nWpLBKsM08cckiW6HzQBtxn/ABfPX9K4JFPlq8YKYWTcGAGcdSKVvhDdTu0TSbFA/F7kdamLo2+0W/omxkyBTxnj+tSt7lrq9EN06Qqpys5Y8HH86Zsn06wt0ieZZ5JAVeTd75Ixt+e9Xzy2sSXCBm80eoKuMYz7VkvGju2zO3PGRVBhkV8RglyRjaOc07b6eiQXE12iyXBORGz+kcdSO9X+XYzW4095o4HVfMSRslRx+HPz/WlJbC0gKhZTdEsodFXAK55A5ovp4rNvLwZNqny1THoPQA0tb3jeQPqYEkTO6JSM7T81r6eLd9Y0tjcusqzJuD8AHcOg+elYv8Zun1Lz5I1SEkjy4xgAe2B04xxWi+sw3VqI5JJGuZZMzSH0hh1AquXhUBdAB0EbZ4PPNVdM7V/cVbuE1uhncIkTYLMcfPP/AD2q+cQabay3Qt2ZFYsGVciXJHQkc9qTvD9TJDNHAYXm4Zl5G7rnHamEuvNmV4twCINwAzkj3H7VfLCqRLdNJthYnfjgg/aonVksbARW5jfO5gwUZYnuaxobya4khjmdQVyecAE/c96aa3dbsyqgWJl3yDBIDD261o6QmnTzGWaby3X0qHPAPfHt3zSN5LBeXdzcQylkDbUVhjKjjP8AzvWeAYHcrkhugDYwe4rX0hHfWLQyF2la5iB3DGcMuP5Cs7ywePeuiP07eMfpVqYAKgjFSJILY9Q6cDmtmUWUUaRS2QeHAJE3IY44JHHf9anHrTPFa28djtgQbFWUZXHPBHt0zWfdo/1Injb+zd9iKegwOciiRYNouoCpKHDxDr8/bvilbvYxIVmdsltzHpn2FL+XvA5HT2qiSBGZXYggcgEZyftV8twyJLFCSd4ADsuMd+KqDyvbqshywIJYDk+1ciUqu0j3zkiuncMkHBHQ1oaTO7azpgkRW/vMfqPXO4c0sy469RQAuAD/AJ1MrhucYPSmmtHt4hNK7QEeuPcMb/tVdzq9lO4WS1laNFJUI3Jb2BJ6is4avcGKcXAdkfGMHkDPTNash+qtolR5CyAsWJAwP9sU5pxhvIks7ySO2UuXabbySAeDVGo2EVhchI7uK4jZN6SRggHP3rNI2sMZ2nt7VWFHOeg6GuOnBK849j7ULnbtPpx3rvIPqbAqBYDjHHQU7pCka5p+eguI/wD2Fa1zorfR27Q2N2ZyRv3RNzx/i44/KqV0i73E/wAPkA6Y8uT9qnaaRfAyZtpY2JKhjHIMAjr06UDSNRkkMVzDJMkeSHaOTBPHTjP/AMqkeGbuWMgwFG3kbzFJkjn46VGHw9do22S0eQMuRmKQBf061CXR9YhTcsNw+/qscbnH34ql9P1qTO+zvmz1zC37Ufw3V8DOn3hPTPkt+1c/hmq/+NvP9F6YGhakcf3aXnvG/wC1TfQb0oMWcqt7nZIc/pS0ujalGQBYXL8ZJWF+P0qB0rVD10y7/wBF6idI1MnjTbsDt5LftTukaTqY1uwaSwuwouIySYWwBuHxX6Jooooooooooooooooooooooooooooooor/2Q==" alt="" /></p>

<h4 id="minor-criticisms-1">Minor Criticisms</h4>

<p>Given that the NFDI4Chem knowledge graph was only deployed in earnest a few
weeks ago and one of its first demos was at the NFDI4Chem Consortium 6.0
Meeting, it’s still lacking in documentation, examples, schematic diagrams, and
other training materials to support people like me who want to navigate the
knowledge graph and construct SPARQL queries - I have high hopes that this will
be forthcoming.</p>

<h3 id="querying-mappings">Querying Mappings</h3>

<p>While SSSOM is usually stored in TSV, it has a specification for converting to
RDF, which means that mappings encoded in SSSOM can be leveraged in SPARQL
queries through federation.</p>

<p>However, there wasn’t already a CLI tool for spinning up a lightweight SPARQL
endpoint based on SSSOM, so I contributed one to
<a href="github.com/mapping-commons/sssom-py">sssom-py</a>, the first-party SSSOM Python
package, in
<a href="https://github.com/mapping-commons/sssom-py/pull/619">mapping-commons/sssom-py#619</a>.
It works by combining the built-in functionality to create an in-memory RDF
cache using <a href="https://github.com/RDFLib/rdflib">RDFlib</a> with Vincent Emonet’s
package <a href="https://github.com/vemonet/rdflib-endpoint">rdflib-endpoint</a>, which
serves a web application based on <a href="https://fastapi.tiangolo.com/">FastAPI</a> that
has a SPARQL endpoint for an RDFlib graph and a user interface via
<a href="https://github.com/TriplyDB/Yasgui">YASGUI</a>.</p>

<p>Here’s a one-liner to deploy the mappings from the Biomappings project,
including the Iconclass mappings described earlier:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>uv pip <span class="nb">install </span>sssom
<span class="gp">$</span><span class="w"> </span>sssom serve-rdf https://w3id.org/biopragmatics/biomappings/sssom/biomappings.sssom.tsv
</code></pre></div></div>

<p>Here’s an example query that finds mappings using Iconclasses:</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">PREFIX</span><span class="w"> </span><span class="nn">skos</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/2004/02/skos/core#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">owl</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/2002/07/owl#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">sssom</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;https://w3id.org/sssom/&gt;</span><span class="w">

</span><span class="k">SELECT</span><span class="w"> </span><span class="nv">?iconclass</span><span class="w"> </span><span class="nv">?o</span><span class="w"> </span><span class="nv">?justification</span><span class="w">
</span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">?iconclass</span><span class="w"> </span><span class="nn">skos</span><span class="o">:</span><span class="ss">relatedMatch</span><span class="w"> </span><span class="nv">?o</span><span class="w"> </span><span class="p">.</span><span class="w">
    </span><span class="k">FILTER</span><span class="w"> </span><span class="err">STARTSWITH</span><span class="p">(</span><span class="nv">?iconclass</span><span class="p">,</span><span class="w"> </span><span class="s2">"https://iconclass.org/"</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>It can also be expanded to use reifed information, like the mapping
justification:</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">PREFIX</span><span class="w"> </span><span class="nn">skos</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/2004/02/skos/core#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">owl</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/2002/07/owl#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">sssom</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;https://w3id.org/sssom/&gt;</span><span class="w">

</span><span class="k">SELECT</span><span class="w"> </span><span class="nv">?iconclass</span><span class="w"> </span><span class="nv">?o</span><span class="w"> </span><span class="nv">?justification</span><span class="w">
</span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="p">[]</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">owl</span><span class="o">:</span><span class="ss">Axiom</span><span class="w"> </span><span class="p">;</span><span class="w">
        </span><span class="nn">owl</span><span class="o">:</span><span class="ss">annotatedSource</span><span class="w"> </span><span class="nv">?iconclass</span><span class="w"> </span><span class="p">;</span><span class="w">
        </span><span class="nn">owl</span><span class="o">:</span><span class="ss">annotatedProperty</span><span class="w"> </span><span class="nn">skos</span><span class="o">:</span><span class="ss">relatedMatch</span><span class="w"> </span><span class="p">;</span><span class="w">
        </span><span class="nn">owl</span><span class="o">:</span><span class="ss">annotatedTarget</span><span class="w"> </span><span class="nv">?o</span><span class="w"> </span><span class="p">;</span><span class="w">
        </span><span class="nn">sssom</span><span class="o">:</span><span class="ss">mapping_justification</span><span class="w"> </span><span class="nv">?justification</span><span class="w"> </span><span class="p">.</span><span class="w">

    </span><span class="k">FILTER</span><span class="w"> </span><span class="err">STARTSWITH</span><span class="p">(</span><span class="nv">?iconclass</span><span class="p">,</span><span class="w"> </span><span class="s2">"https://iconclass.org/"</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>These were some big interludes! Now would be a great time for making a joke
about either Brandon Sanderson books or the official Wind Waker strategy guide
that said there’s actually a lot of stuff to do before going to fight Ganondorf
underwater. But, let’s push forward.</p>

<h2 id="part-4-asking-multidisciplinary-questions">Part 4: Asking Multidisciplinary Questions</h2>

<p>Finally, you’ve arrived at the big conclusion. What can do by bridging the
chemistry knowledge graph and culture knowledge graph? We can connect datasets
in Chemotion electronic laboratory notebooks (ELNs) that annotate the instrument
used for generation with depictions of those instruments in cultural heritage
objects like paintings.</p>

<p>Is this the most practical query? No. But it’s cool, and more importantly, it
demonstrates a few things:</p>

<ol>
  <li>Even a mundane question might require federating many different resources</li>
  <li>NFDI has successfully built many detailed resources that support these
questions</li>
  <li>We’re almost at the point where these questions can be operationalized</li>
  <li>Where the NFDI’s semantic stack has gaps, I am ready to contribute my own
technologies. I’m very excited to have recently joined NFDI during summer
2025, and I’m looking forward to helping shape its semantics roadmap</li>
</ol>

<p>Here’s an architectural diagram of the resources that are required for making
the query I described, which highlights the boundaries between resources and the
relationships that connect across them.</p>

<pre><code class="language-mermaid">graph LR
    subgraph nfdi4culture [NFDI4Culture]
        painting[Painting] -- annotated with --&gt; iconclass[Iconclass]
    end

    subgraph biomappings [Biomappings]
        iconclass -. federation -.- iconclass2[Iconclass]
        iconclass2 -- depicts (Biomappings) --&gt; instrumentClass2[Class of Instruments]
    end

    subgraph chmo [CHMO Ontology]
        instrumentClass[Class of Instruments] -. federation -.- instrumentClass2
        instrument[Instrument] -- participates in --&gt; measurement[Measurement]
        instrument -- is a --&gt; instrumentClass
    end

    subgraph nfdi4chem [NFDI4Chem]
        measurement-. federation -.- measurement2[Measurement]
        measurement2[Measurement] -- appears in experiment --&gt; experiment[Experiment]
    end
</code></pre>

<p>Finally, to tie up the whole post, here’s what I imagine the SPARQL that would
encode this query would look like. I say this in the conjunctive because this
SPARQL won’t actually run - it relies on the existence of two services that
don’t yet exist:</p>

<ol>
  <li>An endpoint that serves semantic mappings, like I described above</li>
  <li>An endpoint that serves ontologies.</li>
</ol>

<p>I got creative here, and imagined that we might have a Base4NFDI service for
mappings in the future. We could also extend the
<a href="https://github.com/INCATools/ubergraph">Ubergraph</a> to include CHMO to make this
query work, or potentially incorporate an extension of this service into the
Terminologies for NFDI (TS4NFDI).</p>

<p>Without further ado, here’s the query. There are notes inside it explaining what
it’s doing.</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">PREFIX</span><span class="w"> </span><span class="nn">prov</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/ns/prov#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">rdf</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/1999/02/22-rdf-syntax-ns#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">owl</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/2002/07/owl#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">BFO</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://purl.obolibrary.org/obo/BFO_&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">skos</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/2004/02/skos/core#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">cto</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;https://nfdi4culture.de/ontology#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">schema</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://schema.org/image&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">iconclass</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;https://iconclass.org/&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">rdfs</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/2000/01/rdf-schema#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">CHMO</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://purl.obolibrary.org/obo/CHMO_&gt;</span><span class="w">

</span><span class="k">SELECT</span><span class="w"> </span><span class="nv">?dataset</span><span class="w"> </span><span class="nv">?experiment</span><span class="w"> </span><span class="nv">?instrument</span><span class="w"> </span><span class="nv">?painting</span><span class="w"> </span><span class="nv">?paintingLabel</span><span class="w"> </span><span class="nv">?paintingURL</span><span class="w">
</span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="c1"># This subquery connects datasets, experiments, and the</span><span class="w">
    </span><span class="c1"># measurement processes using the NFDI4Chem knowledge graph</span><span class="w">
    </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">&lt;https://search.nfdi4chem.de/sparql&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">?dataset</span><span class="w"> </span><span class="nn">prov</span><span class="o">:</span><span class="ss">wasGeneratedBy</span><span class="o">/</span><span class="nn">prov</span><span class="o">:</span><span class="ss">used</span><span class="w"> </span><span class="nv">?experiment</span><span class="w"> </span><span class="p">.</span><span class="w">
        </span><span class="nv">?experiment</span><span class="w"> </span><span class="nn">prov</span><span class="o">:</span><span class="ss">wasGeneratedBy</span><span class="o">/</span><span class="nn">rdf</span><span class="o">:</span><span class="ss">type</span><span class="w"> </span><span class="nv">?measurmentProcess</span><span class="w"> </span><span class="p">.</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c1"># This subquery connects measurement processes to the instruments</span><span class="w">
    </span><span class="c1"># that participate (BFO:0000057) in them. It fictionally uses</span><span class="w">
    </span><span class="c1"># Ubergraph as a service, but this won't work because it doesn't</span><span class="w">
    </span><span class="c1"># yet incorporate CHMO</span><span class="w">
    </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">&lt;https://ubergraph.apps.renci.org/sparql&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="c1"># this could be extended further to do variable</span><span class="w">
        </span><span class="c1"># hierchical traversal of measurement processes'</span><span class="w">
        </span><span class="c1"># and instruments' subclassess, but skipped for</span><span class="w">
        </span><span class="c1"># brevity</span><span class="w">
        </span><span class="nv">?measurmentProcess</span><span class="w"> </span><span class="nn">rdfs</span><span class="o">:</span><span class="ss">subClassOf</span><span class="w"> </span><span class="p">[</span><span class="w">
            </span><span class="nn">rdf</span><span class="o">:</span><span class="ss">type</span><span class="w"> </span><span class="nn">owl</span><span class="o">:</span><span class="ss">Restriction</span><span class="p">;</span><span class="w">
            </span><span class="nn">owl</span><span class="o">:</span><span class="ss">onProperty</span><span class="w"> </span><span class="nn">BFO</span><span class="o">:</span><span class="ss">0000057</span><span class="w"> </span><span class="p">;</span><span class="w">
            </span><span class="nn">owl</span><span class="o">:</span><span class="ss">someValuesFrom</span><span class="w"> </span><span class="nv">?instrument</span><span class="w">
        </span><span class="p">]</span><span class="w"> </span><span class="p">.</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c1"># This subquery uses SSSOM from Biomappings to look up relationships</span><span class="w">
    </span><span class="c1"># between iconclasses and instruments. It fictionally imagines a</span><span class="w">
    </span><span class="c1"># Base4NFDI service that serves semantic mappings via SPARQL.</span><span class="w">
    </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">&lt;https://mappings.services.base4nfdi.de/sparql&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">?iconclass</span><span class="w"> </span><span class="nn">skos</span><span class="o">:</span><span class="ss">relatedMatch</span><span class="w"> </span><span class="nv">?instrument</span><span class="w"> </span><span class="p">.</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c1"># Missing: variable-length hierarchical traversal of the</span><span class="w">
    </span><span class="c1"># iconclasses</span><span class="w">

    </span><span class="c1"># The final subquery connects iconclasses to paintings</span><span class="w">
    </span><span class="c1"># and their corresponding URLs for depiction via the</span><span class="w">
    </span><span class="c1"># NFDI4Culture knowledge graph</span><span class="w">
    </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">&lt;https://nfdi4culture.de/sparql&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">?painting</span><span class="w"> </span><span class="nn">cto</span><span class="o">:</span><span class="ss">subjectConcept</span><span class="w"> </span><span class="nv">?iconclass</span><span class="w"> </span><span class="p">;</span><span class="w">
                  </span><span class="nn">schema</span><span class="o">:</span><span class="ss">image</span><span class="w"> </span><span class="nv">?paintingURL</span><span class="w"> </span><span class="p">.</span><span class="w">
        </span><span class="k">FILTER</span><span class="w"> </span><span class="nb">STRSTARTS</span><span class="p">(</span><span class="nb">STR</span><span class="p">(</span><span class="nv">?iconclass</span><span class="p">),</span><span class="w"> </span><span class="nb">STR</span><span class="p">(</span><span class="nn">iconclass</span><span class="o">:</span><span class="p">))</span><span class="w">
        </span><span class="k">OPTIONAL</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">?painting</span><span class="w"> </span><span class="nn">rdfs</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="nv">?paintingLabel</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<p>Like many of my favorite blog posts, this one was also something of a
misadventure. After hearing Thorsten’s talk, I whispered to Philip, who was
sitting next to me, something like, <em>neat! I bet we could code that up tonight
in an evening hacking session</em>.</p>

<p>We didn’t quite get around to that because the consortium meeting was also a
great opportunity to socialize with our excellent colleagues who are typically
scattered throughout Germany. I did give it a shot after the nightly activities
were over and made a <a href="https://www.youtube.com/watch?v=WMWaryo3w4s">screencast</a>
of the steps in preparing Iconclass for PyOBO. Maybe you’ll find it interesting
watching me code - it’s a relatively quiet video with no narration except the
occasional expletive (you’ve been warned). I got as far as the first attempt at
generating semantic mappings from Iconclass to CHMO, OBI, and ChEBI.</p>

<p>As you might have guessed from reading, it actually ended up taking several
weeks to make improvements to different pieces of software, to curate new data,
and to write it all up. And, it’s not even working yet. So it goes!</p>

<hr />

<p>Double post-script, wow! If you’re in the NFDI sphere and have ideas for other
cross-disciplinary queries, please let me know. I am excited to extend this
demonstration, especially to something more practical.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="knowledge graphs" /><category term="SPARQL" /><category term="chemistry" /><category term="culture" /><category term="cultural heritage" /><category term="NFDI" /><summary type="html"><![CDATA[At the sixth NFDI4Chem consortium meeting, Torsten Schrade from the NFDI4Culture consortium gave a lovely and whimsical talk entitled A Data Alchemist’s Journey through NFDI which explored ways that we might federate and jointly query both consortia’s knowledge via their respective SPARQL endpoints. He proposed a toy example in which he linked paintings depicting alchemists trying to make gold to compounds containing gold. This post is about the steps I took to automate his toy example and extend it to not only chemicals or compounds represented in Iconclass, but also equipment and devices.]]></summary></entry><entry><title type="html">Representing Negative Knowledge</title><link href="https://cthoyt.com/2025/10/07/negative-rdf.html" rel="alternate" type="text/html" title="Representing Negative Knowledge" /><published>2025-10-07T21:18:00+00:00</published><updated>2025-10-07T21:18:00+00:00</updated><id>https://cthoyt.com/2025/10/07/negative-rdf</id><content type="html" xml:base="https://cthoyt.com/2025/10/07/negative-rdf.html"><![CDATA[<p>Representing negative knowledge in the semantic web is an open problem. This
post is going to be a living document where I keep notes on use cases, potential
solutions, and awful hacks.</p>

<h2 id="sssom">SSSOM</h2>

<p>In the
<a href="https://mapping-commons.github.io/sssom/">Simple Standard for Sharing Ontological Mappings (SSSOM)</a>,
we decided to represent negative knowledge by adding a modifier onto a
predicate. This means that a semantic mapping known to be false gets represented
like this in TSV:</p>

<pre><code class="language-tsv">subject_id	subject-label	predicate_id	predicate_modifier	object_id	object_label	mapping_justification
CHEBI:8925	ryanodine	skos:exactMatch	Not	mesh:D031309	Ryania	semapv:ManualMappingCuration
</code></pre>

<p>Since SSSOM is heavily tied to the OBO community, this gets turned into RDF with
the following axiom:</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[]</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">owl:</span><span class="n">Axiom</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">owl:</span><span class="n">annotatedSubject</span><span class="w"> </span><span class="nn">CHEBI:</span><span class="mi">8925</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">owl:</span><span class="n">annotatedProperty</span><span class="w"> </span><span class="nn">skos:</span><span class="n">exactMatch</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">owl:</span><span class="n">annotatedObject</span><span class="w"> </span><span class="nn">mesh:</span><span class="n">D031309</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">sssom:</span><span class="n">predicate_modifier</span><span class="w"> </span><span class="nn">sssom:</span><span class="n">NegatedPredicate</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>It’s debated whether asserting an axiom also requires the assertion of the
triple itself <code class="language-plaintext highlighter-rouge">CHEBI:8925 skos:exactMatch mesh:D031309</code>, however, in this case,
it’s clear that triples represent true knowledge, so we would not want to
concretize it.</p>

<h2 id="rdf-surfaces">RDF Surfaces</h2>

<p>Damien Goutte-Gattat made me aware of
<a href="https://www.semantic-web-journal.net/system/files/swj3799.pdf">RDF Surfaces</a> in
a
<a href="https://github.com/mapping-commons/sssom/pull/469/files#r2382049109">discussion on a SSSOM PR</a>.
To summarize his comment, RDF surfaces create a part of an RDF graph that
contains negated assertions, something like:</p>

<pre><code class="language-n3">@prefix log: &lt;http://www.w3.org/2000/10/swap/log#&gt; .
@prefix FBbt: &lt;http://purl.obolibrary.org/obo/FBbt_&gt; .
@prefix UBERON: &lt;http://purl.obolibrary.org/obo/FBbt_&gt; .
@prefix skos: http://www.w3.org/2004/02/skos/core#&gt; .

(_:x) log:onNegativeSurface {
  FBbt:00004508 skos:exactMatch UBERON:0000056 .
} .
</code></pre>

<p>This paper is still
<a href="https://www.semantic-web-journal.net/content/rdf-surfaces-enabling-classical-negation-and-first-order-expressivity-semantic-web">under review</a>
and also might be of limited use because it uses sneaky N3 syntax <code class="language-plaintext highlighter-rouge">{</code> <code class="language-plaintext highlighter-rouge">}</code> which
are used to express <a href="https://www.w3.org/TeamSubmission/n3/#Quoting">formulae</a>.</p>

<hr />

<p>I haven’t actually gone out to do a deep survey on this. If you are aware of
something relevant, please let me know.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="knowledge graphs" /><summary type="html"><![CDATA[Representing negative knowledge in the semantic web is an open problem. This post is going to be a living document where I keep notes on use cases, potential solutions, and awful hacks.]]></summary></entry><entry><title type="html">Suggesting new relations in ROR from Wikidata</title><link href="https://cthoyt.com/2025/09/25/enriching-ror-with-wikidata.html" rel="alternate" type="text/html" title="Suggesting new relations in ROR from Wikidata" /><published>2025-09-25T15:35:00+00:00</published><updated>2025-09-25T15:35:00+00:00</updated><id>https://cthoyt.com/2025/09/25/enriching-ror-with-wikidata</id><content type="html" xml:base="https://cthoyt.com/2025/09/25/enriching-ror-with-wikidata.html"><![CDATA[<p>I was looking at the different NFDI consortia in the
<a href="https://ror.org">Research Organization Registry (ROR)</a>, and found that the only
two that have a parent relations to the
<a href="https://bioregistry.io/ror:05qj6w324">NFDI (<code class="language-plaintext highlighter-rouge">ror:05qj6w324</code>)</a> are
<a href="https://bioregistry.io/ror:00bb4nn95">NFDI4DS (<code class="language-plaintext highlighter-rouge">ror:00bb4nn95</code>)</a> and
<a href="https://bioregistry.io/ror:04ncnzm65">MaRDI (<code class="language-plaintext highlighter-rouge">ror:04ncnzm65</code>)</a>. This felt
strange to me, so I started looking around Wikidata to see if I could
automatically make a curation sheet to send along to them. I found that Wikidata
already has detailed pages for all NFDI consortia, and that they also include
relationships to the parent. This blog post is about the steps I took to write a
workflow to find relationships in Wikidata that are appropriate for submission
to ROR.</p>

<h2 id="getting-wikidata">Getting Wikidata</h2>

<p>In Wikidata, an entity can be annotated with a ROR identifier via property
<code class="language-plaintext highlighter-rouge">P6782</code>. I wanted to write a SPARQL query for the to retrieve all triples for
which both the subject and object have and ROR identifier.</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="nv">?subject</span><span class="w"> </span><span class="nv">?subjectROR</span><span class="w"> </span><span class="nv">?subjectLabel</span><span class="w"> </span><span class="nv">?predicate</span><span class="w"> </span><span class="nv">?object</span><span class="w"> </span><span class="nv">?objectROR</span><span class="w"> </span><span class="nv">?objectLabel</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nv">?subject</span><span class="w"> </span><span class="nv">?predicate</span><span class="w"> </span><span class="nv">?object</span><span class="w"> </span><span class="p">;</span><span class="w">
           </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P6782</span><span class="w"> </span><span class="nv">?subjectROR</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="nv">?object</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P6782</span><span class="w"> </span><span class="nv">?objectROR</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">bd</span><span class="o">:</span><span class="ss">serviceParam</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">language</span><span class="w"> </span><span class="s2">"[AUTO_LANGUAGE],mul,en"</span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>In the first version of this post, I ran the SPARQL queries against first-party
<a href="https://query.wikidata.org/">Wikidata Query Service (WQS)</a> (permalink to this
query at <a href="https://w.wiki/FUVA">https://w.wiki/FUVA</a>) and ran into timeout
issues. This motivated the following section of this post on the exploration of
the underlying triples and alternate query formulations below.</p>

<p>However, both Egon (<a href="https://mastodon.social/@egonw/115281836981009390">here</a>)
and Tiago
(<a href="https://github.com/cthoyt/cthoyt.github.io/issues/78#issuecomment-3345745218">here</a>)
suggested I also try the University of Freiburg’s QLever mirror of Wikidata
available at
<a href="https://qlever.cs.uni-freiburg.de/wikidata">https://qlever.cs.uni-freiburg.de/wikidata</a>
(permalink to this query at
<a href="https://qlever.cs.uni-freiburg.de/wikidata/Vz75fY?exec=true">https://qlever.cs.uni-freiburg.de/wikidata/Vz75fY</a>).</p>

<p>QLever is blazingly fast and returns about 67K rows in only a few seconds. That
all being said, I retained my exploration below for posterity.</p>

<h2 id="working-around-timeouts-on-the-wqs">Working around timeouts on the WQS</h2>

<p>The next step in any investigation with a blasphemous
<code class="language-plaintext highlighter-rouge">?subject ?predicate ?object</code> pattern is to look into the predicates and try to
cut them down. I set to reformulating the query to count the frequency of
appearance of each predicate.</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="nv">?p</span><span class="w"> </span><span class="nv">?pLabel</span><span class="w"> </span><span class="p">(</span><span class="nb">COUNT</span><span class="p">(</span><span class="nv">?p</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nv">?count</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nv">?subject</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P6782</span><span class="w"> </span><span class="nv">?subjectROR</span><span class="p">;</span><span class="w">
           </span><span class="nv">?predicate</span><span class="w"> </span><span class="nv">?object</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="nv">?object</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P6782</span><span class="w"> </span><span class="nv">?objectROR</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="nv">?p</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">directClaim</span><span class="w"> </span><span class="nv">?predicate</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">bd</span><span class="o">:</span><span class="ss">serviceParam</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">language</span><span class="w"> </span><span class="s2">"[AUTO_LANGUAGE],mul,en"</span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="nv">?p</span><span class="w"> </span><span class="nv">?pLabel</span><span class="w">
</span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">DESC</span><span class="p">(</span><span class="nv">?count</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>This query uses the sneaky <code class="language-plaintext highlighter-rouge">wikibase:directClaim</code> to map between the <code class="language-plaintext highlighter-rouge">wd:</code>
entity namespace and <code class="language-plaintext highlighter-rouge">wdt:</code> direct property namespace so the query service could
look up the label for the link. The problem was, this query was still too heavy
and caused a timeout. Therefore, I had to simplify the query to just get the
counts without the label, then use a second query and join the data externally
(I also tried a nested query along the way, but it still timed out).</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="nv">?predicate</span><span class="w"> </span><span class="p">(</span><span class="nb">COUNT</span><span class="p">(</span><span class="nv">?predicate</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nv">?count</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nv">?subject</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P6782</span><span class="w"> </span><span class="nv">?subjectROR</span><span class="w"> </span><span class="p">;</span><span class="w">
           </span><span class="nv">?predicate</span><span class="w"> </span><span class="nv">?object</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="nv">?object</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P6782</span><span class="w"> </span><span class="nv">?objectROR</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="nv">?predicate</span><span class="w">
</span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">DESC</span><span class="p">(</span><span class="nv">?count</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>With that out of the way, I tried re-writing the original query by formatting in
the 147 predicates I pulled out into the <code class="language-plaintext highlighter-rouge">VALUES ?predicate { ... }</code>
(abbreviated), like:</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">
</span><span class="k">SELECT</span><span class="w"> </span><span class="nv">?subject</span><span class="w"> </span><span class="nv">?subjectROR</span><span class="w"> </span><span class="nv">?subjectLabel</span><span class="w"> </span><span class="nv">?predicate</span><span class="w"> </span><span class="nv">?object</span><span class="w"> </span><span class="nv">?objectROR</span><span class="w"> </span><span class="nv">?objectLabel</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="k">VALUES</span><span class="w"> </span><span class="nv">?predicate</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="nv">?subject</span><span class="w"> </span><span class="nv">?predicate</span><span class="w"> </span><span class="nv">?object</span><span class="w"> </span><span class="p">;</span><span class="w">
           </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P6782</span><span class="w"> </span><span class="nv">?subjectROR</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="nv">?object</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P6782</span><span class="w"> </span><span class="nv">?objectROR</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">bd</span><span class="o">:</span><span class="ss">serviceParam</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">language</span><span class="w"> </span><span class="s2">"[AUTO_LANGUAGE],mul,en"</span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>This still caused timeouts, so I resorted to a loop in Python, which also let me
simplify the query to skip the Wikidata IDs and just pull out RORs for the
subject and object (where the <code class="language-plaintext highlighter-rouge">{...}</code> gets replaced with a different property on
each):</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="nv">?subjectROR</span><span class="w"> </span><span class="nv">?objectROR</span><span class="w">
</span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">?subjectROR</span><span class="w"> </span><span class="err">^</span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P6782</span><span class="o">/</span><span class="nn">wdt</span><span class="o">:</span><span class="p">{...}</span><span class="o">/</span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P6782</span><span class="w"> </span><span class="nv">?objectROR</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>I really like this because it uses paths to reduce the need to specify the
middle entities which don’t get used. I don’t know if the SPARQL engine is able
to optimize on it, but it’s cool. Maybe not so readable, but cool. The loop
created a super-sized TSV with the predicate and labels added back.</p>

<p>The workflow I implemented for this lives in
<a href="https://github.com/cthoyt/ror-wikidata-enrichment">https://github.com/cthoyt/ror-wikidata-enrichment</a>.
The data from Wikidata is in
<a href="https://github.com/cthoyt/ror-wikidata-enrichment/blob/main/data/3-wikidata-ror-relations.tsv">this file</a>,
licensed under CC0.</p>

<p>Do you want this workflow to better reflect your organization? Check out my
other blog post on <a href="/2021/01/17/organization-organization.html">how to curate data about your research
organization</a></p>

<h2 id="getting-ror">Getting ROR</h2>

<p>I’ve previously implemented a source in
<a href="https://github.com/biopragmatics/pyobo">PyOBO</a> that wraps downloading and
structuring ROR’s data dump into a readily usable format, so getting ROR’s
triples was as easy as:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pyobo</span>

<span class="n">df</span> <span class="o">=</span> <span class="n">pyobo</span><span class="p">.</span><span class="n">get_relations_df</span><span class="p">(</span><span class="s">"ror"</span><span class="p">)</span>
</code></pre></div></div>

<p>I also had to map the part of and has part relations from BFO to Wikidata
properties. I did this by hand because it was faster than doing it the
sustainable way, which would have been to pull the mappings from SSSOM-like
annotations in the BFO ontology or from Wikidata itself (since I curated those
into Wikidata years ago when we were preparing the (unpublished) relation
ontology paper).</p>

<p>I made an intermediate output of all of thet triples
<a href="https://github.com/cthoyt/ror-wikidata-enrichment/raw/refs/heads/main/data/4-ror-relations.tsv">here</a>,
licensed under CC0.</p>

<h2 id="putting-it-all-together">Putting it all together</h2>

<p>While I’m glossing over a few steps that you can grok by reading
<a href="https://github.com/cthoyt/ror-wikidata-enrichment/blob/main/main.py">my python script</a>,
it was possible to finish getting the data in the right shape to compare with
tools in PyOBO and
<a href="https://github.com/biopragmatics/bioregistry.">the Bioregistry</a></p>

<p>The final step was to take the difference between the Wikidata triples and the
ROR triples, filter for triples that make sense within the ROR schema (which for
now is just part of and has part relationships), and then dump the results out.
There were around 67K records before filtering around 2.8K after filtering. Here
are a few examples:</p>

<table>
  <thead>
    <tr>
      <th>subjectROR</th>
      <th>subjectLabel</th>
      <th>predicate</th>
      <th>predicateLabel</th>
      <th>objectROR</th>
      <th>objectLabel</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>00k4nrj32</td>
      <td>Essex County Hospital</td>
      <td>P361</td>
      <td>part of</td>
      <td>02wnqcb97</td>
      <td>National Health Service</td>
    </tr>
    <tr>
      <td>022efad20</td>
      <td>University of Gabès</td>
      <td>P527</td>
      <td>has part(s)</td>
      <td>01hwc7828</td>
      <td>Institut des Régions Arides</td>
    </tr>
    <tr>
      <td>04p4gjp18</td>
      <td>Center of Excellence on Hazardous Substance Management</td>
      <td>P361</td>
      <td>part of</td>
      <td>028wp3y58</td>
      <td>Chulalongkorn University</td>
    </tr>
    <tr>
      <td>04tnv7w23</td>
      <td>École Supérieure Polytechnique d’Antsiranana</td>
      <td>P361</td>
      <td>part of</td>
      <td>00pd4qq98</td>
      <td>Université d’Antsiranana</td>
    </tr>
    <tr>
      <td>02f4ya153</td>
      <td>Barro Colorado Island</td>
      <td>P361</td>
      <td>part of</td>
      <td>01pp8nd67</td>
      <td>Smithsonian Institution</td>
    </tr>
  </tbody>
</table>

<h2 id="coda">Coda</h2>

<p>The point of all of this was to automate adding the missing NFDI consortia
relationships to the parent NFDI organization in ROR, because I’m interested in
creating queries over the organization landscape related to NFDI to support an
upcoming section on Internationalization. And like most things in my work life,
I ended up cleaning some data and making upstream contributions along the way.
Let’s see how receptive ROR is to this! The triples are all
<a href="https://github.com/cthoyt/ror-wikidata-enrichment/blob/main/data/6-diff-suggestions.tsv">here</a>
and I can easily make them a different format for submission.</p>

<hr />

<p>Caveat: if you look into the data, you might notice that some of the entities
don’t have labels. I realized this is happening because I haven’t updated my
PyOBO importer to get the 2.0 data dump from ROR, and I’m stuck on old version
1.36. This can be fixed independently of this workflow. Here’s the rows related
to the NFDI consortia that need new relations, which are all missing labels
until I fix this.</p>

<table>
  <thead>
    <tr>
      <th>subjectROR</th>
      <th>subjectLabel</th>
      <th>predicate</th>
      <th>predicateLabel</th>
      <th>objectROR</th>
      <th>objectLabel</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>00enhv193</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>02cxb1m07</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>03xrvbe74</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>020tty630</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>04ncnzm65</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>01f5dqg10</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>001jhv750</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>0310v3480</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>01d2qgg03</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>01k9z4a50</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>03a4sp974</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>05wwzbv21</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>0305k8y39</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>0238fds33</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>03f6sdf65</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>0033j3009</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>01vnkaz16</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>01v7r4v08</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>04dy2xw62</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>01xptp363</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>034pbpe12</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>05nfk7108</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>00r0qs524</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>00bb4nn95</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
    <tr>
      <td>03fqpzb44</td>
      <td> </td>
      <td>P361</td>
      <td>part of</td>
      <td>05qj6w324</td>
      <td>Nationale Forschungsdateninfrastruktur</td>
    </tr>
  </tbody>
</table>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="ROR" /><category term="Wikidata" /><category term="organizations" /><category term="bibliometrics" /><summary type="html"><![CDATA[I was looking at the different NFDI consortia in the Research Organization Registry (ROR), and found that the only two that have a parent relations to the NFDI (ror:05qj6w324) are NFDI4DS (ror:00bb4nn95) and MaRDI (ror:04ncnzm65). This felt strange to me, so I started looking around Wikidata to see if I could automatically make a curation sheet to send along to them. I found that Wikidata already has detailed pages for all NFDI consortia, and that they also include relationships to the parent. This blog post is about the steps I took to write a workflow to find relationships in Wikidata that are appropriate for submission to ROR.]]></summary></entry><entry><title type="html">Switching from using Tox to Just</title><link href="https://cthoyt.com/2025/09/21/tox-to-just.html" rel="alternate" type="text/html" title="Switching from using Tox to Just" /><published>2025-09-21T09:21:00+00:00</published><updated>2025-09-21T09:21:00+00:00</updated><id>https://cthoyt.com/2025/09/21/tox-to-just</id><content type="html" xml:base="https://cthoyt.com/2025/09/21/tox-to-just.html"><![CDATA[<p>I became aware of <a href="https://github.com/casey/just"><code class="language-plaintext highlighter-rouge">just</code></a> while watching Hynek’s
<a href="https://youtu.be/TiBIjouDGuI?si=dh3HFkXx_RThdsEq&amp;t=741">second video on <code class="language-plaintext highlighter-rouge">uv</code></a> a
few months ago. I immediately fell in love with its elegance and simplicity, so
I have begun replacing task running in my repositories that relied on
<a href="https://github.com/tox-dev/tox"><code class="language-plaintext highlighter-rouge">tox</code></a> with <code class="language-plaintext highlighter-rouge">just</code>. This post gives a bit of
background, context, and walks through making the switch on one of my
repositories that has some annoying dependencies.</p>

<h2 id="what-is-tox">What is Tox</h2>

<p><a href="https://github.com/tox-dev/tox"><code class="language-plaintext highlighter-rouge">tox</code></a> is a tool tailored for developing Python
packages. It takes care of creating a virtual environment for the configuration
then installing the current package with the specified extras and dependency
groups. Here’s what my testing configuration in the
<a href="https://github.com/cthoyt/ssslm/blob/129200609ad6dc9254112364c6ba96bc0b08a1c6/tox.ini#L33-L51"><code class="language-plaintext highlighter-rouge">ssslm</code></a>
package looked like:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[testenv]</span>
<span class="py">description</span> <span class="p">=</span> <span class="s">Run unit and integration tests.</span>
<span class="py">commands</span> <span class="p">=</span>
    <span class="err">coverage</span> <span class="err">run</span> <span class="err">-p</span> <span class="err">-m</span> <span class="err">pytest</span> <span class="py">--durations</span><span class="p">=</span><span class="s">20 {posargs:tests}</span>
    <span class="err">coverage</span> <span class="err">combine</span>
    <span class="err">coverage</span> <span class="err">xml</span>
<span class="py">extras</span> <span class="p">=</span>
    <span class="err">gilda-slim</span>
    <span class="err">web</span>
    <span class="err">scispacy</span>
    <span class="err">gliner</span>
    <span class="err">rdflib</span>
    <span class="err">ontology</span>
    <span class="err">pandas</span>
<span class="py">dependency_groups</span> <span class="p">=</span>
    <span class="err">tests</span>
    <span class="err">en-core-sci-sm</span>
</code></pre></div></div>

<p>This can be run with <code class="language-plaintext highlighter-rouge">tox -e py</code> after installing tox (e.g., <code class="language-plaintext highlighter-rouge">uvx tox -e py</code> or
<code class="language-plaintext highlighter-rouge">uvx --with tox-uv tox -e py</code> to enable virtual environment creation with uv)</p>

<p>Tox can actually do a lot more things than this, including skipping installing
the current repository as a Python package to instead install/run development
tools like <code class="language-plaintext highlighter-rouge">ruff</code>. It also has the ability to refer from the parts of one
configuration to another, which is useful for having different flavors of the
same command (e.g., documentation build vs. documentation test).</p>

<p>Check the Tox documentation or see the <code class="language-plaintext highlighter-rouge">tox.ini</code> file in SSSLM v0.1.2, the last
version before I switched from Tox to Just:
<a href="https://github.com/cthoyt/ssslm/blob/v0.1.2/tox.ini">https://github.com/cthoyt/ssslm/blob/v0.1.2/tox.ini</a>.</p>

<h2 id="what-is-just">What is Just</h2>

<p><a href="https://github.com/casey/just"><code class="language-plaintext highlighter-rouge">just</code></a> is a more generic tool for writing
tasks, which is a much better fit than Makefiles - read the Just homepage for
excellent arguments which I won’t recapitulate here. Here’s what the previous
Tox configuration looks using Just’s custom syntax in a file called <code class="language-plaintext highlighter-rouge">justfile</code>,
this time using uv to do the heavy lifting:</p>

<pre><code class="language-justfile">[doc("run unit and integration tests")]
test:
    just coverage erase
    uv run --group tests --all-extras --no-extra scispacy --no-extra gilda -m coverage run -p -m pytest
    just coverage combine
    just coverage xml

[doc("run `coverage` with a given subcommand")]
@coverage command:
    uvx --from coverage[toml] coverage {{command}}
</code></pre>

<p>This can be run with <code class="language-plaintext highlighter-rouge">just test</code> after installing just (e.g.,
<code class="language-plaintext highlighter-rouge">uvx --from rust-just just test</code>).</p>

<p>Note that <code class="language-plaintext highlighter-rouge">uv</code> is now doing the heavy lifting for environment management and
installation instead of Tox, and Just simply ties it all together.</p>

<h2 id="making-the-switch">Making the Switch</h2>

<p>I finally got around to testing replacing running my tests with tox with running
my tests with just in
<a href="https://github.com/cthoyt/ssslm/pull/32">cthoyt/ssslm#32</a>. I did this because:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">tox</code> is feeling very slow these days. The <code class="language-plaintext highlighter-rouge">tox-uv</code> extension was a nice
respite, but it only uses <code class="language-plaintext highlighter-rouge">uv</code> for virtual environment construction (iirc)
and doesn’t take advantage of the other fast parts of uv.</li>
  <li><code class="language-plaintext highlighter-rouge">tox</code> creates a wastefully large cache in every project</li>
  <li><code class="language-plaintext highlighter-rouge">uvx</code> is a more fit-for-purpose solution for many of the things I use <code class="language-plaintext highlighter-rouge">tox</code>
for, i.e., installing and running a tool in an isolated virtual environment</li>
</ol>

<p>I had to start with two other PRs to SSSLM
(<a href="https://github.com/cthoyt/ssslm/pull/31">cthoyt/ssslm#31</a> and
<a href="https://github.com/cthoyt/ssslm/pull/33">cthoyt/ssslm#33</a>) that did some
restructuring of tests and the way dependencies were declared such that it would
even be possible to run tests without all extras/groups installed.</p>

<p>Then, I had to look into the issue caused by
<a href="https://github.com/allenai/scispacy/">ScispaCy</a>, which is running based on
notoriously old dependencies (only works on Python &lt;= 3.12 and with NumPy &lt;
2.0). FYI, I’m not just complaining, I’ve been making upstream PRs to their
repository to help get to broader compatibility.</p>

<p>The biggest problem with old NumPy dependencies is they’re often either 1) not
available as a wheel or 2) difficult to compile in an automated setting.</p>

<p>One solution is to use the <code class="language-plaintext highlighter-rouge">tool.uv.conflicts</code> configuration to say that the
<code class="language-plaintext highlighter-rouge">pandas</code> and <code class="language-plaintext highlighter-rouge">scispacy</code> extras can’t be installed at the same time because their
(transitive) dependencies have conflicts. Don’t confuse these names with the
packages - the extras include a list of related things that are defined in the
<code class="language-plaintext highlighter-rouge">project.optional-dependencies</code> block of my project configuration.</p>

<p>Ideally, I wanted my test configuration in my <code class="language-plaintext highlighter-rouge">justfile</code> to have two
back-to-back calls to <code class="language-plaintext highlighter-rouge">uv run</code> like this:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>uv run <span class="nt">--group</span> tests <span class="nt">--all-extras</span> <span class="nt">--no-extra</span> scispacy <span class="nt">--no-extra</span> gilda <span class="nt">-m</span> coverage run <span class="nt">-p</span> <span class="nt">-m</span> pytest
<span class="gp">$</span><span class="w"> </span>uv run <span class="nt">--group</span> tests <span class="nt">--group</span> en-core-sci-sm <span class="nt">--extra</span> scispacy <span class="nt">--extra</span> ontology <span class="nt">-m</span> coverage run <span class="nt">-p</span> <span class="nt">-m</span> pytest
</code></pre></div></div>

<p>The first command runs most tests, and the second one just installs the scispacy
extras and runs that. However, this didn’t work (it didn’t seem to manage to
install the <code class="language-plaintext highlighter-rouge">scispacy</code> extra). I think there is a solution for this, but I am
still learning about the nuances of <code class="language-plaintext highlighter-rouge">uv run</code> and uv’s notion of locking.</p>

<p>I tabled getting ScispaCy tests for SSSLM to work for now, since this is not a
generic concern of most packages. The next steps are to test replacing all Tox
environments with corresponding just commands in SSSLM, then upstream this to my
cookiecutter template
<a href="https://github.com/cthoyt/cookiecutter-snekpack">https://github.com/cthoyt/cookiecutter-snekpack</a>
so all of my repositories can benefit.</p>

<h2 id="what-we-really-want">What We Really Want</h2>

<p>It would be great if <code class="language-plaintext highlighter-rouge">uv</code> had a built-in notion of task definitions, since most
of the things I had in my <code class="language-plaintext highlighter-rouge">tox</code> configuration (and now <code class="language-plaintext highlighter-rouge">justfile</code>) are calls to
install Python environments or run Python things inside them (this is true for
testing, linting, documentation building, publishing, etc.).</p>

<p>There’s a long-standing issue on their tracker
<a href="https://github.com/astral-sh/uv/issues/5903">astral-sh/uv#5903</a> that I’m sure
will be addressed in the future, when they’ve made an excellent design for the
developer experience. I’m looking forward to the future where I can write a
follow-up post entitled <em>Switching from <code class="language-plaintext highlighter-rouge">just</code> + <code class="language-plaintext highlighter-rouge">uv</code> to just <code class="language-plaintext highlighter-rouge">uv</code></em> (wordplay
intended).</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="packaging" /><category term="Python" /><category term="Tox" /><category term="just" /><category term="snekpack" /><category term="automation" /><category term="CI/CD" /><summary type="html"><![CDATA[I became aware of just while watching Hynek’s second video on uv a few months ago. I immediately fell in love with its elegance and simplicity, so I have begun replacing task running in my repositories that relied on tox with just. This post gives a bit of background, context, and walks through making the switch on one of my repositories that has some annoying dependencies.]]></summary></entry><entry><title type="html">Exploring an unfamiliar SPARQL endpoint with the Bioregistry - a case study from NFDI4Culture</title><link href="https://cthoyt.com/2025/09/11/nfdi4culture-prefix-validation.html" rel="alternate" type="text/html" title="Exploring an unfamiliar SPARQL endpoint with the Bioregistry - a case study from NFDI4Culture" /><published>2025-09-11T10:26:00+00:00</published><updated>2025-09-11T10:26:00+00:00</updated><id>https://cthoyt.com/2025/09/11/nfdi4culture-prefix-validation</id><content type="html" xml:base="https://cthoyt.com/2025/09/11/nfdi4culture-prefix-validation.html"><![CDATA[<p>Earlier this week at the sixth NFDI4Chem consortium meeting,
<a href="https://www.adwmainz.de/personen/mitarbeiterinnen/profil/torsten-schrade.html">Torsten Schrade</a>
from the NFDI4Culture consortium gave a lovely and whimsical talk entitled <em>A
Data Alchemist’s Journey through NFDI</em> which explored ways that we might
federate and jointly query both consortia’s knowledge via their respective
SPARQL endpoints. This post is about the very first steps I took when looking
into this new (to me) SPARQL endpoint, namely to identify what prefixes and
semantic spaces are present, then
<a href="https://github.com/biopragmatics/bioregistry/pull/1691">added a new CLI tool</a>
to <a href="https://bioregistry.io">the Bioregistry</a> to do this reproducibly.</p>

<p>The NFDI4Culture’s SPARQL endpoint <code class="language-plaintext highlighter-rouge">https://nfdi4culture.de/sparql</code> is wrapped
by a nice user interface
<a href="https://nfdi4culture.de/resources/knowledge-graph#">here</a> for interactive
querying in the browser.</p>

<p><a href="https://nfdi4culture.de/resources/knowledge-graph#"><img src="/img/nfdi4culture-sparql.png" alt="A screenshot of the NFDI4Culture SPARQL endpoint's web-based user interface" /></a></p>

<p>It has two example queries to
<a href="https://nfdi4culture.de/go/sparql-repositories-ta4">list all research data repositories</a>
and to
<a href="https://nfdi4culture.de/go/sparql-data-portals">list all research data portals</a>,
but otherwise I’m a bit stuck to better understand its schema.</p>

<h2 id="checking-the-prefix-map">Checking the Prefix Map</h2>

<p>However, the NFDI4Culture SPARQL endpoint based on Virtuoso, so there is a way
to look at what are the default CURIE prefixes and URI prefixes by navigating to
<a href="https://nfdi4culture.de/sparql/?help=nsdecl">here</a>.</p>

<p>In my <a href="/2025/09/04/bioregistry-turtle-validation.html">previous post</a>,
I demonstrated generalizing the notion of prefix map validation to incorporate
prefix maps from either JSON-LD or in the beginning of turtle files.</p>

<p>I extended this even further to extract the prefix map from the Virtuoso SPARQL
endpoint page in
<a href="https://github.com/biopragmatics/bioregistry/pull/1691">biopragmatics/bioregistry#1691</a>.
Note that this doesn’t work for <em>all</em> triple stores, it just works on Virtuoso
because of the way that it provides a special page for showing the prefix map. I
wasn’t able to find a way to get it directly, so the implementation does HTML
scraping and parsing.</p>

<p>Here’s how you can use the validator I wrote:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>bioregistry validate virtuoso https://nfdi4culture.de/sparql <span class="nt">--tablefmt</span> github
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>prefix</th>
      <th>uri_prefix</th>
      <th>issue</th>
      <th>solution</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>as</td>
      <td>https://www.w3.org/ns/activitystreams#</td>
      <td>unknown CURIE prefix</td>
      <td>Switch to CURIE prefix ac, inferred from URI prefix</td>
    </tr>
    <tr>
      <td>bif</td>
      <td>http://www.openlinksw.com/schemas/bif#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>dawgt</td>
      <td>http://www.w3.org/2001/sw/DataAccess/tests/test-dawg#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>dbpprop</td>
      <td>http://dbpedia.org/property/</td>
      <td>unknown CURIE prefix</td>
      <td>Switch to CURIE prefix dbpedia.property, inferred from URI prefix</td>
    </tr>
    <tr>
      <td>fn</td>
      <td>http://www.w3.org/2005/xpath-functions/#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>formats</td>
      <td>http://www.w3.org/ns/formats/</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>gqi</td>
      <td>http://www.openlinksw.com/schemas/graphql/intro#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>gql</td>
      <td>http://www.openlinksw.com/schemas/graphql#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>gr</td>
      <td>http://purl.org/goodrelations/v1#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>ldp</td>
      <td>http://www.w3.org/ns/ldp#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>math</td>
      <td>http://www.w3.org/2000/10/swap/math#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>nci</td>
      <td>http://ncicb.nci.nih.gov/xml/owl/EVS/Thesaurus.owl#</td>
      <td>non-standard CURIE prefix</td>
      <td>Switch to standard prefix: ncit</td>
    </tr>
    <tr>
      <td>ogc</td>
      <td>http://www.opengis.net/</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>ogcgml</td>
      <td>http://www.opengis.net/ont/gml#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>ogcgs</td>
      <td>http://www.opengis.net/ont/geosparql#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>ogcgsf</td>
      <td>http://www.opengis.net/def/function/geosparql/</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>ogcgsr</td>
      <td>http://www.opengis.net/def/rule/geosparql/</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>ogcsf</td>
      <td>http://www.opengis.net/ont/sf#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>product</td>
      <td>http://www.buy.com/rss/module/productV2/</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>protseq</td>
      <td>http://purl.org/science/protein/bysequence/</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>rdfdf</td>
      <td>http://www.openlinksw.com/virtrdf-data-formats#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>sc</td>
      <td>http://purl.org/science/owl/sciencecommons/</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>scovo</td>
      <td>http://purl.org/NET/scovo#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>sd</td>
      <td>http://www.w3.org/ns/sparql-service-description#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>sioc</td>
      <td>http://rdfs.org/sioc/ns#</td>
      <td>unknown CURIE prefix</td>
      <td>Switch to CURIE prefix sioc.core, inferred from URI prefix</td>
    </tr>
    <tr>
      <td>sql</td>
      <td>http://www.openlinksw.com/schemas/sql#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>stat</td>
      <td>http://www.w3.org/ns/posix/stat#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>vcard2006</td>
      <td>http://www.w3.org/2006/vcard/ns#</td>
      <td>unknown CURIE prefix</td>
      <td>Switch to CURIE prefix vcard, inferred from URI prefix</td>
    </tr>
    <tr>
      <td>virtcxml</td>
      <td>http://www.openlinksw.com/schemas/virtcxml#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>virtrdf</td>
      <td>http://www.openlinksw.com/schemas/virtrdf#</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>xf</td>
      <td>http://www.w3.org/2004/07/xpath-functions</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>xsl10</td>
      <td>http://www.w3.org/XSL/Transform/1.0</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>xsl1999</td>
      <td>http://www.w3.org/1999/XSL/Transform</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>xslwd</td>
      <td>http://www.w3.org/TR/WD-xsl</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
    <tr>
      <td>yago</td>
      <td>http://dbpedia.org/class/yago/</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<h2 id="interpreting-the-results">Interpreting the results</h2>

<p>Some of the key takeaways from this table are:</p>

<ol>
  <li>The feedback on <code class="language-plaintext highlighter-rouge">as</code> is a false positive - a look at
https://www.w3.org/ns/activitystreams# shows that the W3 standard wants <code class="language-plaintext highlighter-rouge">as</code>
to be the preferred prefix</li>
  <li>There are several true positive suggestions, like fixing the <code class="language-plaintext highlighter-rouge">ncit</code> prefix.</li>
  <li>There’s a whole group of URI spaces using <code class="language-plaintext highlighter-rouge">opengis.net</code> from the
<a href="http://www.opengeospatial.org">Open Geospatial Consortium</a>, dealing with
geospatial data that could be registered in the Bioregistry</li>
  <li>There are a large number URI spaces from <code class="language-plaintext highlighter-rouge">openlinksw.com</code>, which correspond
to Virtuoso itself. These might be good to put in the Bioregistry, but are
all very short, which means that there is high potential for overlap.
However, there aren’t any reported conflicts</li>
  <li>There are several <code class="language-plaintext highlighter-rouge">w3.org</code> standard prefixes that should be registered in the
Bioregistry.</li>
  <li>There are several prefixes that I’m not familiar with that also use short
acronyms, which makes me a bit hesitant to add in the Bioregistry. In these
cases, I usually either don’t add them because of lack of wide-spread use or
add them using a less generic prefix.</li>
</ol>

<hr />

<p>Something I still want to implement is a generic workflow for identifying
putative URI spaces in a given remote SPARQL endpoint. If there are only a small
number of triples that can be exhaustively queried, then
<a href="https://curies.readthedocs.io/en/latest/discovery.html">this workflow</a> can be
used. Otherwise, I am still considering different options.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="NFDI" /><category term="SPARQL" /><category term="Bioregistry" /><summary type="html"><![CDATA[Earlier this week at the sixth NFDI4Chem consortium meeting, Torsten Schrade from the NFDI4Culture consortium gave a lovely and whimsical talk entitled A Data Alchemist’s Journey through NFDI which explored ways that we might federate and jointly query both consortia’s knowledge via their respective SPARQL endpoints. This post is about the very first steps I took when looking into this new (to me) SPARQL endpoint, namely to identify what prefixes and semantic spaces are present, then added a new CLI tool to the Bioregistry to do this reproducibly.]]></summary></entry><entry><title type="html">Validating the FAIRness of knowledge graphs and ontologies in RDF using the Bioregistry</title><link href="https://cthoyt.com/2025/09/04/bioregistry-turtle-validation.html" rel="alternate" type="text/html" title="Validating the FAIRness of knowledge graphs and ontologies in RDF using the Bioregistry" /><published>2025-09-04T12:30:00+00:00</published><updated>2025-09-04T12:30:00+00:00</updated><id>https://cthoyt.com/2025/09/04/bioregistry-turtle-validation</id><content type="html" xml:base="https://cthoyt.com/2025/09/04/bioregistry-turtle-validation.html"><![CDATA[<p>Using standard CURIE prefixes and URI prefixes in semantic web artifacts such as
<a href="https://en.wikipedia.org/wiki/Resource_Description_Framework">Resource Description Framework (RDF)</a>
promotes interoperability, enables reuse in downstream data integration, and
makes data more FAIR. The <a href="https://bioregistry.io">Bioregistry</a> defines a set of
standard CURIE prefixes and URI prefixes against which RDF files can be
validated/standardized. This blog post describes a new CLI tool
<code class="language-plaintext highlighter-rouge">bioregistry validate ttl</code> in the Bioregistry Python package that can run
validation on <a href="https://en.wikipedia.org/wiki/Turtle_(syntax)">Turtle</a> files (a
common serialization of RDF).</p>

<p>RDF data stored in Turtle files typically begins with a stanza defining a prefix
map. For example, one of the Turtle files in the
<a href="https://github.com/ISE-FIZKarlsruhe/chemotion-kg/tree/4cb5c24af6494d66fb8cd849921131dbc789c163&gt;">Chemotion Knowledge Graph (Chemotion-KG)</a>
begins with the following six prefixes:</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@prefix</span><span class="w"> </span><span class="nn">nfdicore:</span><span class="w"> </span><span class="nl">&lt;https://nfdi.fiz-karlsruhe.de/ontology/&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">ns1:</span><span class="w"> </span><span class="nl">&lt;http://purls.helmholtz-metadaten.de/mwo/&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">ns2:</span><span class="w"> </span><span class="nl">&lt;http://purl.obolibrary.org/obo/chebi/&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">obo:</span><span class="w"> </span><span class="nl">&lt;http://purl.obolibrary.org/obo/&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">rdfs:</span><span class="w"> </span><span class="nl">&lt;http://www.w3.org/2000/01/rdf-schema#&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">xsd:</span><span class="w"> </span><span class="nl">&lt;http://www.w3.org/2001/XMLSchema#&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>The following command can be used to validate it. Using <code class="language-plaintext highlighter-rouge">--tablefmt github</code>
results in a nice table that can be formatted into a blog post, otherwise it
outputs text in a more vertical format.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>bioregistry validate ttl https://github.com/ISE-FIZKarlsruhe/chemotion-kg/raw/4cb5c24af/processing/output_bfo_compliant.ttl
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>prefix</th>
      <th>uri_prefix</th>
      <th>issue</th>
      <th>solution</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>nfdicore</td>
      <td>https://nfdi.fiz-karlsruhe.de/ontology/</td>
      <td>non-standard CURIE prefix</td>
      <td>Switch to standard prefix: nfdi.core</td>
    </tr>
    <tr>
      <td>ns1</td>
      <td>http://purls.helmholtz-metadaten.de/mwo/</td>
      <td>unknown CURIE prefix</td>
      <td>Consider switching to the more specific CURIE/URI prefix pair mwo: <code class="language-plaintext highlighter-rouge">http://purls.helmholtz-metadaten.de/mwo/mwo_</code></td>
    </tr>
    <tr>
      <td>ns2</td>
      <td>http://purl.obolibrary.org/obo/chebi/</td>
      <td>unknown CURIE prefix</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<p>I was able to directly
<a href="https://github.com/ISE-FIZKarlsruhe/chemotion-kg/issues/2">open an issue</a> on
the GitHub repository to give feedback. In general, I think this is a very
powerful use of the Bioregistry because it can support groups interested in
making knowledge graphs and ontologies towards improving their data and
ultimately making it more FAIR.</p>

<hr />

<p>In case you’re interested in how I implemented this,
<a href="https://github.com/biopragmatics/bioregistry/pull/1670">check this PR</a>. I was
able to reuse some ideas from a previous JSON-LD validator, extend them, and
abstract the code. Later, I will be able to implement similar validators for XML
files, ontologies, and any other resource from which I can extract a prefix map.</p>

<p>I also left a TODO inside the code, since this can be extended with several
other ways of validating URI prefixes. Ultimately, this may get upstreamed into
the <a href="https://github.com/biopragmatics/curies"><code class="language-plaintext highlighter-rouge">curies</code></a> package to make it even
more accessible to groups making their own prefix maps or using custom instances
of the Bioregistry.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="CURIEs" /><category term="URIs" /><category term="URNs" /><category term="IRIs" /><category term="identifiers" /><category term="identifier schema" /><category term="Bioregistry" /><category term="biopragmatics" /><summary type="html"><![CDATA[Using standard CURIE prefixes and URI prefixes in semantic web artifacts such as Resource Description Framework (RDF) promotes interoperability, enables reuse in downstream data integration, and makes data more FAIR. The Bioregistry defines a set of standard CURIE prefixes and URI prefixes against which RDF files can be validated/standardized. This blog post describes a new CLI tool bioregistry validate ttl in the Bioregistry Python package that can run validation on Turtle files (a common serialization of RDF).]]></summary></entry><entry><title type="html">A historical analysis of ChEMBL</title><link href="https://cthoyt.com/2025/08/26/chembl-history.html" rel="alternate" type="text/html" title="A historical analysis of ChEMBL" /><published>2025-08-26T15:44:00+00:00</published><updated>2025-08-26T15:44:00+00:00</updated><id>https://cthoyt.com/2025/08/26/chembl-history</id><content type="html" xml:base="https://cthoyt.com/2025/08/26/chembl-history.html"><![CDATA[<p>I’ve recently submitted an article to the
<a href="https://joss.theoj.org/">Journal of Open Source Software (JOSS)</a> describing
<a href="https://github.com/cthoyt/"><code class="language-plaintext highlighter-rouge">chembl-downloader</code></a>, a Python package for
automating downloading and using ChEMBL data in a reproducible way. In this
post, I use <code class="language-plaintext highlighter-rouge">chembl-downloader</code> to show how the number of compounds, assays,
activities, and other entities in ChEMBL have changed over time.</p>

<p>ChEMBL has made 37 releases so far. 35 of them have been major releases, and two
have been minor releases
(<a href="https://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases/chembl_22_1/">v22.1</a>
and
<a href="https://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases/chembl_24_1/">v24.1</a>).
While it only began bundling a SQLite dump of the database
<a href="https://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases/chembl_19/">v19</a>,
Eloy Felix
<a href="https://github.com/cthoyt/chembl-downloader/issues/20">recently informed me</a>
that the team constructed SQLite dumps for all previous versions, too. This is
great, because I use the SQLite dump as the primary mechanism for querying the
database.</p>

<p><code class="language-plaintext highlighter-rouge">chembl-downloader</code> automates downloading any version of ChEMBL’s SQLite
database, unpacking it from the TAR archive, connecting to it, making a SQL
query, and returning the results as a Pandas DataFrame object with
<a href="https://chembl-downloader.readthedocs.io/en/latest/api/chembl_downloader.query.html"><code class="language-plaintext highlighter-rouge">chembl_downloader.query()</code></a>
or
<a href="https://chembl-downloader.readthedocs.io/en/latest/api/chembl_downloader.query_scalar.html"><code class="language-plaintext highlighter-rouge">chembl_downloader.query_scalar()</code></a>
like in:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">chembl_downloader</span>

<span class="n">sql</span> <span class="o">=</span> <span class="s">"SELECT * FROM activities LIMIT 5"</span>
<span class="n">rows</span> <span class="o">=</span> <span class="n">chembl_downloader</span><span class="p">.</span><span class="n">query</span><span class="p">(</span><span class="n">sql</span><span class="p">,</span> <span class="n">version</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>

<span class="n">sql</span> <span class="o">=</span> <span class="s">"SELECT COUNT(*) FROM activities"</span>
<span class="n">count</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="n">chembl_downloader</span><span class="p">.</span><span class="n">query_scalar</span><span class="p">(</span><span class="n">sql</span><span class="p">,</span> <span class="n">version</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
</code></pre></div></div>

<p>These functions can be used to write a loop and run the same SQL query over
every version of ChEMBL with a few two caveats:</p>

<ol>
  <li>to get <em>all</em> versions of ChEMBL, it needs to include v22.1 and v24.1.
<a href="https://chembl-downloader.readthedocs.io/en/latest/api/chembl_downloader.versions.html"><code class="language-plaintext highlighter-rouge">chembl.versions()</code></a>
provides convenient access to construct this iterator</li>
  <li>SQLite does not allow for opening a compressed database
(<a href="https://www.sqlite.org/zipvfs/doc/trunk/www/readme.wiki">without paying for an extension</a>),
so each version needs to be uncompressed. Unfortunately, most personal
computers (including mine) don’t enough hard disk space to have an
uncompressed copy of each version of ChEMBL</li>
</ol>

<h2 id="results-of-temporal-analysis">Results of Temporal Analysis</h2>

<p>I wrote a CLI utility
<a href="https://chembl-downloader.readthedocs.io/en/latest/cli.html#chembl-downloader-history"> <code class="language-plaintext highlighter-rouge">chembl_downloader history</code></a>
which downloads, decompresses, analyzes, and then deletes each version of ChEMBL
iteratively over the span of about three hours.</p>

<p>It summarizes the dates of release, number of compounds, number of named
compounds, (i.e., with a <code class="language-plaintext highlighter-rouge">pref_name</code>), number of assays, and number of
activities, and several other entity types to (almost) match what’s summarized
on the ChEMBL <a href="https://www.ebi.ac.uk/chembl">homepage</a>. The results can be
<a href="https://github.com/cthoyt/chembl-downloader/raw/refs/heads/main/docs/_data/summary.tsv">downloaded as a TSV</a>
and are as follows:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: right">Version</th>
      <th>Date</th>
      <th style="text-align: right">Compounds</th>
      <th style="text-align: right">Named Compounds</th>
      <th style="text-align: right">Assays</th>
      <th style="text-align: right">Activities</th>
      <th style="text-align: right">Documents</th>
      <th style="text-align: right">Targets</th>
      <th style="text-align: right">Cells</th>
      <th style="text-align: right">Tissues</th>
      <th style="text-align: right">Drug Warnings</th>
      <th style="text-align: right">Drug Indications</th>
      <th style="text-align: right">Drug Mechanisms</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: right">35</td>
      <td>2024-12-01</td>
      <td style="text-align: right">2,496,335</td>
      <td style="text-align: right">42,231</td>
      <td style="text-align: right">1,740,546</td>
      <td style="text-align: right">21,123,501</td>
      <td style="text-align: right">92,121</td>
      <td style="text-align: right">16,003</td>
      <td style="text-align: right">2,129</td>
      <td style="text-align: right">782</td>
      <td style="text-align: right">1,676</td>
      <td style="text-align: right">55,442</td>
      <td style="text-align: right">7,330</td>
    </tr>
    <tr>
      <td style="text-align: right">34</td>
      <td>2024-03-28</td>
      <td style="text-align: right">2,431,025</td>
      <td style="text-align: right">42,387</td>
      <td style="text-align: right">1,644,390</td>
      <td style="text-align: right">20,772,701</td>
      <td style="text-align: right">89,892</td>
      <td style="text-align: right">15,598</td>
      <td style="text-align: right">2,023</td>
      <td style="text-align: right">782</td>
      <td style="text-align: right">1,676</td>
      <td style="text-align: right">55,442</td>
      <td style="text-align: right">7,330</td>
    </tr>
    <tr>
      <td style="text-align: right">33</td>
      <td>2023-05-31</td>
      <td style="text-align: right">2,399,743</td>
      <td style="text-align: right">41,923</td>
      <td style="text-align: right">1,610,596</td>
      <td style="text-align: right">20,334,684</td>
      <td style="text-align: right">88,630</td>
      <td style="text-align: right">15,398</td>
      <td style="text-align: right">2,021</td>
      <td style="text-align: right">782</td>
      <td style="text-align: right">1,636</td>
      <td style="text-align: right">51,582</td>
      <td style="text-align: right">7,098</td>
    </tr>
    <tr>
      <td style="text-align: right">32</td>
      <td>2023-01-26</td>
      <td style="text-align: right">2,354,965</td>
      <td style="text-align: right">41,923</td>
      <td style="text-align: right">1,536,903</td>
      <td style="text-align: right">20,038,828</td>
      <td style="text-align: right">86,361</td>
      <td style="text-align: right">15,139</td>
      <td style="text-align: right">2,015</td>
      <td style="text-align: right">759</td>
      <td style="text-align: right">1,636</td>
      <td style="text-align: right">51,582</td>
      <td style="text-align: right">7,098</td>
    </tr>
    <tr>
      <td style="text-align: right">31</td>
      <td>2022-07-12</td>
      <td style="text-align: right">2,331,700</td>
      <td style="text-align: right">41,585</td>
      <td style="text-align: right">1,498,681</td>
      <td style="text-align: right">19,780,369</td>
      <td style="text-align: right">85,431</td>
      <td style="text-align: right">15,072</td>
      <td style="text-align: right">2,000</td>
      <td style="text-align: right">757</td>
      <td style="text-align: right">1,293</td>
      <td style="text-align: right">48,816</td>
      <td style="text-align: right">6,656</td>
    </tr>
    <tr>
      <td style="text-align: right">30</td>
      <td>2022-02-22</td>
      <td style="text-align: right">2,157,379</td>
      <td style="text-align: right">41,549</td>
      <td style="text-align: right">1,458,215</td>
      <td style="text-align: right">19,286,751</td>
      <td style="text-align: right">84,092</td>
      <td style="text-align: right">14,855</td>
      <td style="text-align: right">1,991</td>
      <td style="text-align: right">752</td>
      <td style="text-align: right">1,293</td>
      <td style="text-align: right">48,816</td>
      <td style="text-align: right">6,656</td>
    </tr>
    <tr>
      <td style="text-align: right">29</td>
      <td>2021-07-01</td>
      <td style="text-align: right">2,105,464</td>
      <td style="text-align: right">41,383</td>
      <td style="text-align: right">1,383,553</td>
      <td style="text-align: right">18,635,916</td>
      <td style="text-align: right">81,544</td>
      <td style="text-align: right">14,554</td>
      <td style="text-align: right">1,978</td>
      <td style="text-align: right">743</td>
      <td style="text-align: right">1,262</td>
      <td style="text-align: right">45,902</td>
      <td style="text-align: right">6,202</td>
    </tr>
    <tr>
      <td style="text-align: right">28</td>
      <td>2021-01-15</td>
      <td style="text-align: right">2,086,898</td>
      <td style="text-align: right">41,049</td>
      <td style="text-align: right">1,358,549</td>
      <td style="text-align: right">17,276,334</td>
      <td style="text-align: right">80,480</td>
      <td style="text-align: right">14,347</td>
      <td style="text-align: right">1,950</td>
      <td style="text-align: right">739</td>
      <td style="text-align: right">1,256</td>
      <td style="text-align: right">42,988</td>
      <td style="text-align: right">5,347</td>
    </tr>
    <tr>
      <td style="text-align: right">27</td>
      <td>2020-05-18</td>
      <td style="text-align: right">1,961,462</td>
      <td style="text-align: right">40,834</td>
      <td style="text-align: right">1,221,361</td>
      <td style="text-align: right">16,066,124</td>
      <td style="text-align: right">76,086</td>
      <td style="text-align: right">13,382</td>
      <td style="text-align: right">1,831</td>
      <td style="text-align: right">707</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">37,259</td>
      <td style="text-align: right">5,134</td>
    </tr>
    <tr>
      <td style="text-align: right">26</td>
      <td>2020-02-14</td>
      <td style="text-align: right">1,950,765</td>
      <td style="text-align: right">40,822</td>
      <td style="text-align: right">1,221,311</td>
      <td style="text-align: right">15,996,368</td>
      <td style="text-align: right">76,076</td>
      <td style="text-align: right">13,377</td>
      <td style="text-align: right">1,830</td>
      <td style="text-align: right">707</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">37,259</td>
      <td style="text-align: right">5,070</td>
    </tr>
    <tr>
      <td style="text-align: right">25</td>
      <td>2019-02-01</td>
      <td style="text-align: right">1,879,206</td>
      <td style="text-align: right">39,885</td>
      <td style="text-align: right">1,125,387</td>
      <td style="text-align: right">15,504,603</td>
      <td style="text-align: right">72,271</td>
      <td style="text-align: right">12,482</td>
      <td style="text-align: right">1,670</td>
      <td style="text-align: right">655</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">29,457</td>
      <td style="text-align: right">4,992</td>
    </tr>
    <tr>
      <td style="text-align: right">24.1</td>
      <td>2018-05-01</td>
      <td style="text-align: right">1,828,820</td>
      <td style="text-align: right">39,877</td>
      <td style="text-align: right">1,060,283</td>
      <td style="text-align: right">15,207,914</td>
      <td style="text-align: right">69,861</td>
      <td style="text-align: right">12,091</td>
      <td style="text-align: right">1,667</td>
      <td style="text-align: right">655</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">29,163</td>
      <td style="text-align: right">4,992</td>
    </tr>
    <tr>
      <td style="text-align: right">24</td>
      <td>2018-05-01</td>
      <td style="text-align: right">1,828,820</td>
      <td style="text-align: right">39,877</td>
      <td style="text-align: right">1,060,283</td>
      <td style="text-align: right">15,207,914</td>
      <td style="text-align: right">69,861</td>
      <td style="text-align: right">12,091</td>
      <td style="text-align: right">1,667</td>
      <td style="text-align: right">655</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">29,163</td>
      <td style="text-align: right">4,992</td>
    </tr>
    <tr>
      <td style="text-align: right">23</td>
      <td>2017-05-18</td>
      <td style="text-align: right">1,735,442</td>
      <td style="text-align: right">39,584</td>
      <td style="text-align: right">1,302,147</td>
      <td style="text-align: right">14,675,320</td>
      <td style="text-align: right">67,722</td>
      <td style="text-align: right">11,538</td>
      <td style="text-align: right">1,624</td>
      <td style="text-align: right">125</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">13,504</td>
      <td style="text-align: right">4,305</td>
    </tr>
    <tr>
      <td style="text-align: right">22.1</td>
      <td>2016-11-17</td>
      <td style="text-align: right">1,686,695</td>
      <td style="text-align: right">39,422</td>
      <td style="text-align: right">1,246,683</td>
      <td style="text-align: right">14,371,197</td>
      <td style="text-align: right">65,213</td>
      <td style="text-align: right">11,224</td>
      <td style="text-align: right">1,619</td>
      <td style="text-align: right">111</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">12,573</td>
      <td style="text-align: right">3,834</td>
    </tr>
    <tr>
      <td style="text-align: right">22</td>
      <td>2016-09-28</td>
      <td style="text-align: right">1,686,695</td>
      <td style="text-align: right">39,422</td>
      <td style="text-align: right">1,246,132</td>
      <td style="text-align: right">14,371,219</td>
      <td style="text-align: right">65,213</td>
      <td style="text-align: right">11,224</td>
      <td style="text-align: right">1,619</td>
      <td style="text-align: right">111</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">12,573</td>
      <td style="text-align: right">3,834</td>
    </tr>
    <tr>
      <td style="text-align: right">21</td>
      <td>2015-02-12</td>
      <td style="text-align: right">1,592,191</td>
      <td style="text-align: right">39,347</td>
      <td style="text-align: right">1,212,831</td>
      <td style="text-align: right">13,968,617</td>
      <td style="text-align: right">62,502</td>
      <td style="text-align: right">11,019</td>
      <td style="text-align: right">1,612</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">5,951</td>
      <td style="text-align: right">3,799</td>
    </tr>
    <tr>
      <td style="text-align: right">20</td>
      <td>2015-02-03</td>
      <td style="text-align: right">1,463,270</td>
      <td style="text-align: right">39,016</td>
      <td style="text-align: right">1,148,942</td>
      <td style="text-align: right">13,520,737</td>
      <td style="text-align: right">59,610</td>
      <td style="text-align: right">10,774</td>
      <td style="text-align: right">1,647</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">2,266</td>
    </tr>
    <tr>
      <td style="text-align: right">19</td>
      <td>2014-07-23</td>
      <td style="text-align: right">1,411,786</td>
      <td style="text-align: right">38,910</td>
      <td style="text-align: right">1,106,285</td>
      <td style="text-align: right">12,843,338</td>
      <td style="text-align: right">57,156</td>
      <td style="text-align: right">10,579</td>
      <td style="text-align: right">1,653</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">2,239</td>
    </tr>
    <tr>
      <td style="text-align: right">18</td>
      <td>2014-04-02</td>
      <td style="text-align: right">1,359,508</td>
      <td style="text-align: right">35,817</td>
      <td style="text-align: right">1,042,374</td>
      <td style="text-align: right">12,419,715</td>
      <td style="text-align: right">53,298</td>
      <td style="text-align: right">9,414</td>
      <td style="text-align: right">1,655</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">2,233</td>
    </tr>
    <tr>
      <td style="text-align: right">17</td>
      <td>2013-09-16</td>
      <td style="text-align: right">1,324,941</td>
      <td style="text-align: right">32,692</td>
      <td style="text-align: right">734,201</td>
      <td style="text-align: right">12,077,491</td>
      <td style="text-align: right">51,277</td>
      <td style="text-align: right">9,356</td>
      <td style="text-align: right">1,746</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">2,213</td>
    </tr>
    <tr>
      <td style="text-align: right">16</td>
      <td>2013-05-15</td>
      <td style="text-align: right">1,295,510</td>
      <td style="text-align: right">23,532</td>
      <td style="text-align: right">712,836</td>
      <td style="text-align: right">11,420,351</td>
      <td style="text-align: right">50,095</td>
      <td style="text-align: right">9,844</td>
      <td style="text-align: right">1,432</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td style="text-align: right">15</td>
      <td>2013-01-30</td>
      <td style="text-align: right">1,254,575</td>
      <td style="text-align: right">23,528</td>
      <td style="text-align: right">679,259</td>
      <td style="text-align: right">10,509,572</td>
      <td style="text-align: right">48,735</td>
      <td style="text-align: right">9,570</td>
      <td style="text-align: right">1,432</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td style="text-align: right">14</td>
      <td>2012-07-18</td>
      <td style="text-align: right">1,213,242</td>
      <td style="text-align: right">16,573</td>
      <td style="text-align: right">644,734</td>
      <td style="text-align: right">10,129,256</td>
      <td style="text-align: right">46,133</td>
      <td style="text-align: right">9,003</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td style="text-align: right">13</td>
      <td>2012-02-29</td>
      <td style="text-align: right">1,143,682</td>
      <td style="text-align: right">16,397</td>
      <td style="text-align: right">617,681</td>
      <td style="text-align: right">6,933,068</td>
      <td style="text-align: right">44,682</td>
      <td style="text-align: right">8,845</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td style="text-align: right">12</td>
      <td>2011-11-30</td>
      <td style="text-align: right">1,077,189</td>
      <td style="text-align: right">16,658</td>
      <td style="text-align: right">596,122</td>
      <td style="text-align: right">5,654,847</td>
      <td style="text-align: right">43,418</td>
      <td style="text-align: right">8,703</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td style="text-align: right">11</td>
      <td>2011-06-07</td>
      <td style="text-align: right">1,060,258</td>
      <td style="text-align: right">16,264</td>
      <td style="text-align: right">582,982</td>
      <td style="text-align: right">5,479,146</td>
      <td style="text-align: right">42,516</td>
      <td style="text-align: right">8,603</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td style="text-align: right">10</td>
      <td>2011-06-07</td>
      <td style="text-align: right">1,000,468</td>
      <td style="text-align: right">16,159</td>
      <td style="text-align: right">534,391</td>
      <td style="text-align: right">4,668,202</td>
      <td style="text-align: right">40,624</td>
      <td style="text-align: right">8,372</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td style="text-align: right">9</td>
      <td>2011-01-04</td>
      <td style="text-align: right">658,075</td>
      <td style="text-align: right">3,746</td>
      <td style="text-align: right">499,867</td>
      <td style="text-align: right">3,030,317</td>
      <td style="text-align: right">39,094</td>
      <td style="text-align: right">8,091</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td style="text-align: right">8</td>
      <td>2010-11-05</td>
      <td style="text-align: right">636,269</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">488,898</td>
      <td style="text-align: right">2,973,034</td>
      <td style="text-align: right">38,462</td>
      <td style="text-align: right">8,088</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td style="text-align: right">7</td>
      <td>2010-09-03</td>
      <td style="text-align: right">602,500</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">485,095</td>
      <td style="text-align: right">2,948,069</td>
      <td style="text-align: right">38,204</td>
      <td style="text-align: right">8,078</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td style="text-align: right">6</td>
      <td>2010-09-03</td>
      <td style="text-align: right">600,625</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">481,752</td>
      <td style="text-align: right">2,925,588</td>
      <td style="text-align: right">38,029</td>
      <td style="text-align: right">8,054</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td style="text-align: right">5</td>
      <td>2010-06-07</td>
      <td style="text-align: right">578,715</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">459,823</td>
      <td style="text-align: right">2,787,240</td>
      <td style="text-align: right">36,624</td>
      <td style="text-align: right">7,493</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td style="text-align: right">4</td>
      <td>2010-05-26</td>
      <td style="text-align: right">565,245</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">446,645</td>
      <td style="text-align: right">2,705,136</td>
      <td style="text-align: right">35,821</td>
      <td style="text-align: right">7,330</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td style="text-align: right">3</td>
      <td>2010-04-30</td>
      <td style="text-align: right">547,133</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">432,022</td>
      <td style="text-align: right">2,490,742</td>
      <td style="text-align: right">34,982</td>
      <td style="text-align: right">7,330</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td style="text-align: right">2</td>
      <td>2009-12-07</td>
      <td style="text-align: right">517,261</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">416,284</td>
      <td style="text-align: right">2,404,622</td>
      <td style="text-align: right">33,956</td>
      <td style="text-align: right">7,192</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td style="text-align: right">1</td>
      <td>2009-10-28</td>
      <td style="text-align: right">440,055</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">329,250</td>
      <td style="text-align: right">1,936,969</td>
      <td style="text-align: right">26,299</td>
      <td style="text-align: right">5,694</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">0</td>
    </tr>
  </tbody>
</table>

<p>The same results can be viewed as charts:</p>

<p><img src="https://github.com/cthoyt/chembl-downloader/raw/main/docs/_data/summary.svg" alt="" /></p>

<p>These charts show when certain features were introduced, such as cells in v15,
drug indications in v20, tissues in v22, and drug warnings in v28.</p>

<p>The number of named compounds seems to have plateaued in v19 in 2014. This is
strange, considering that ChEMBL links to many external resources like ChEBI
that have nice preferred names that be imported. However, much like I found in
<a href="/2025/04/17/chembl-indications-efo-exploration.html">my recent post</a>
about the EFO identifier column in ChEMBL’s diseases table, the <code class="language-plaintext highlighter-rouge">pref_name</code>
column in the compounds table might not actually mean what I guess it does.</p>

<h2 id="change-over-time">Change over Time</h2>

<p>In order to investigate the changes over time, I also took the discrete
derivative of each:</p>

<p><img src="https://github.com/cthoyt/chembl-downloader/raw/main/docs/_data/summary-diff.svg" alt="" /></p>

<p>There are a few interesting places where the numbers dropped, such as the number
of targets in v17 and the number of assays in v24 (which might have been a
mistake that triggered the v24.1 release). I’m sure there’s a bit of explanation
in the READMEs for these releases - please comment at the end of the post if you
happen to take a look and have more explanation.</p>

<p>Overall, this analysis shows that the amount of content added between ChEMBL
versions is relatively consistent (though keep in mind it’s on a log axis). The
time for each release is also only slightly increasing on average.</p>

<h2 id="future-ideas">Future Ideas</h2>

<p>I would love to extend the idea of a temporal analysis towards other
target-centric metrics like:</p>

<ol>
  <li>Are there examples of targets where the chemical space gets a lot bigger?</li>
  <li>Conversely, are there targets where new compounds just seem to be in the same
old neighborhood?</li>
  <li>Are there widely conflicting activities added over time?</li>
  <li>How does the ability of a QSAR model trained on a given version of ChEMBL
perform with respect to the data that’s added later?</li>
</ol>

<p>I presented one such example in the <code class="language-plaintext highlighter-rouge">chembl-downloader</code> manuscript where I
re-ran one of Pat Walter’s analyses on
<a href="https://bioregistry.io/chembl:CHEMBL4550">5-lipoxygenase activating protein (CHEMBL4550)</a>
in
<a href="https://github.com/cthoyt/chembl-downloader/blob/main/notebooks/refresh-static-data.ipynb">this notebook</a>.
There, the number of activities increased by more than double since the original
analysis, but the distribution was roughly the same.</p>

<p>If you’re interested in teaming up to do a retrospective analysis on your
favorite target (or, maybe even using knowledge graphs for interesting
aggregations of targets based on gene sets, disease associations, etc.), then
let me know.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="ChEMBL" /><category term="cheminformatics" /><category term="chemistry" /><category term="bibliometrics" /><summary type="html"><![CDATA[I’ve recently submitted an article to the Journal of Open Source Software (JOSS) describing chembl-downloader, a Python package for automating downloading and using ChEMBL data in a reproducible way. In this post, I use chembl-downloader to show how the number of compounds, assays, activities, and other entities in ChEMBL have changed over time.]]></summary></entry><entry><title type="html">Measuring the impact of the Bioregistry</title><link href="https://cthoyt.com/2025/08/22/bioregistry-impact.html" rel="alternate" type="text/html" title="Measuring the impact of the Bioregistry" /><published>2025-08-22T14:54:00+00:00</published><updated>2025-08-22T14:54:00+00:00</updated><id>https://cthoyt.com/2025/08/22/bioregistry-impact</id><content type="html" xml:base="https://cthoyt.com/2025/08/22/bioregistry-impact.html"><![CDATA[<p>The <a href="https://bioregistry.io">Bioregistry</a> is a database and toolchain for
standardization of prefixes, CURIEs, and URIs that appear in linked (open) data.
While I created it in 2019 as a component of
<a href="https://github.com/biopragmatics/pyobo">PyOBO</a> in order to support parsing
database cross-references appearing in biomedical ontologies, it has since
become an independent project with a community-driven governance model and much
broader applications. This post is a first attempt to quantify its usage and
impact.</p>

<h2 id="what-are-usage-and-impact">What are Usage and Impact?</h2>

<p>For a foundational resource like the Bioregistry, there are two kinds of usage.
First, direct usage encompasses when a workflow directly reuses the
Bioregistry’s data, software, or web application. For example:</p>

<ol>
  <li>The <a href="https://github.com/mapping-commons/sssom-py">SSSOM-py</a> Python package
for interacting with semantic mappings uses the Bioregistry for supplying a
comprehensive default prefix map during parsing of SSSOM files.</li>
  <li>The <a href="https://www.bridgedb.org/">BridgeDb</a> identifier mapping service uses the
Bioregistry Python package to test its source metadata are properly
standardized
(<a href="https://github.com/bridgedb/datasources/blob/a238b40b915c7f7a69052afecfdc59ad26211b41/scripts/align_bioregistry.py#L6">see here</a>)</li>
  <li>The <a href="https://github.com/protegeproject/protege/">Protegé</a> ontology editor
uses the Bioregistry’s API to look up information about prefixes.</li>
  <li><a href="https://www.wikipathways.org">WikiPathways</a> uses the Bioregistry’s
resolution service to linkify compact URIs ( CURIEs).</li>
</ol>

<p>Second, indirect usage encompasses any other data, software, web application,
etc. that builds on direct usages. For example, any ontology that is edited
using Protegé indirectly uses the Bioregistry, like the
<a href="https://github.com/DiseaseOntology/HumanDiseaseOntology">Disease Ontology (DO)</a>.</p>

<p>I consider usage to be a very good proxy for impact, especially when considering
indirect usage. Allen Baron <em>et al.</em> recently published
<a href="https://doi.org/10.1093/nar/gkad1051">The DO-KB Knowledgebase: a 20-year journey developing the disease open science ecosystem</a>,
which made a quantification of DO’s cumulative impact over the last decades. One
area of this study focused on literature citations and resulted in the
development of a
<a href="https://github.com/DiseaseOntology/DO.utils">reusable, open-source software package</a>
for such analyses. I don’t think anyone would disagree that the DO has had high
impact.</p>

<p>I think that because DO is built using tools that rely on the Bioregistry, it’s
fair to claim that DO’s (recent) impact is partially due to the Bioregistry.</p>

<p>The Bioregistry is in the excellent position where there are a variety of direct
and indirect usages, many of which are highly impactful, meaning that the
Bioregistry can share (a bit) in their glory. Later in this post, I’ll give a
more quantitative justification for that statement.</p>

<h2 id="why-quantify-usage-and-impact">Why Quantify Usage and Impact?</h2>

<p>I think some of the main reasons for quantifying a project’s usage and impact
are to:</p>

<ol>
  <li>justify its continued maintenance and improvement</li>
  <li>get credit for making something important</li>
  <li>use previous usage as examples for increased adoption</li>
  <li>get funding to continue maintaining and improving it</li>
</ol>

<p>At the moment, I am preparing to apply for the Bioregistry to be recognized by
the Global Biodata Coalition as a
<a href="https://globalbiodata.org/what-we-do/global-core-biodata-resources/">Global Core Biodata Resource (GCBR)</a>.
I am also preparing an application on behalf of current affiliation, RWTH Aachen
University, to join the
<a href="https://www.denbi.de">German Network for Bioinformatics Infrastructure (deNBI)</a>,
which will include offering the Bioregistry as key bioinformatics
infrastructure. Both applications require quantitative evidence of the usage and
impact of the proposed resource as part of their respective key performance
indicators (KPIs).</p>

<h2 id="the-bioregistry-by-the-numbers">The Bioregistry by the Numbers</h2>

<p>There are two kinds of statistics I think are important to convey about the
Bioregistry. The first is related to community involvement. I’ve carefully
planned the governance structure of the project to be sustainable (based on
<a href="https://doi.org/10.1038/s41597-024-03406-w">the open data, open code, and open infrastructure (O3) guidelines</a>).
This was successful in no small part due the way that there is a very low
barrier for entry for small, external contributions (i.e., a
<a href="https://doi.org/10.32388/KBX9VO">drive-by curation</a>). Therefore, it makes sense
to highlight the number of unique contributors there have been to the data/code
of the project as well as the volume of contributions in the forms of issues,
discussions, and pull requests:</p>

<table>
  <thead>
    <tr>
      <th>Statistic</th>
      <th style="text-align: right">Count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Contributors</td>
      <td style="text-align: right">75</td>
    </tr>
    <tr>
      <td>Open Issues</td>
      <td style="text-align: right">146</td>
    </tr>
    <tr>
      <td>Closed Issues</td>
      <td style="text-align: right">471</td>
    </tr>
    <tr>
      <td>Total Issues</td>
      <td style="text-align: right">617</td>
    </tr>
    <tr>
      <td>Open pull requests</td>
      <td style="text-align: right">13</td>
    </tr>
    <tr>
      <td>Closed pull requests</td>
      <td style="text-align: right">1,012</td>
    </tr>
    <tr>
      <td>Total pull requests</td>
      <td style="text-align: right">1,025</td>
    </tr>
  </tbody>
</table>

<p>The second kind of statistic that’s important describes the content of the
resource itself.</p>

<table>
  <thead>
    <tr>
      <th>Statistic</th>
      <th style="text-align: right">Count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Prefixes</td>
      <td style="text-align: right">2,024</td>
    </tr>
    <tr>
      <td>Prefix Synonyms</td>
      <td style="text-align: right">547</td>
    </tr>
    <tr>
      <td>External positive mappings</td>
      <td style="text-align: right">9,162</td>
    </tr>
    <tr>
      <td>External negative mappings</td>
      <td style="text-align: right">157</td>
    </tr>
    <tr>
      <td>Total external mappings</td>
      <td style="text-align: right">9,319</td>
    </tr>
    <tr>
      <td>Collections</td>
      <td style="text-align: right">18</td>
    </tr>
    <tr>
      <td>External Registries</td>
      <td style="text-align: right">33</td>
    </tr>
  </tbody>
</table>

<p>Note that this table is from mid-August 2025. Most of these numbers increase
over time. To give some further context to these statistics, here’s a chart that
shows how the Bioregistry stacks up against other related resources. Keep in
mind, the Bioregistry is also a meta-resource that incorporates their important
parts, too. You can understand this chart by looking at the percentage in
parentheses (like +146% over Identifiers.org) and think: wow, those numbers are
much bigger than 0%, which means the Bioregistry is much more comprehensive!</p>

<p><img src="https://raw.githubusercontent.com/biopragmatics/bioregistry/main/docs/img/bioregistry_coverage_bar.svg" alt="" /></p>

<h2 id="direct-usage">Direct Usage</h2>

<h3 id="who-uses-the-bioregistry-website">Who uses the Bioregistry Website?</h3>

<p>The Bioregistry website exposes metadata about ontologies, databases, and other
resources that mint identifiers. This enables biocurators, data stewards,
librarians, and others interested in identifying appropriate ontologies for
reuse in their resources, data management plans, etc. It also allows researchers
who consume linked (open) data to find context about the kinds of prefixes,
CURIEs, or URIs that appear.</p>

<p>The following chart breaks down the nearly 4,400 unique users of the
Bioregistry’s website by country. It shows that the Bioregistry is having a
global impact, with potential for further growth in Africa (which I hope to do
by getting in touch with the
<a href="http://www.africapidalliance.org">Africa PID Alliance</a>).</p>

<p><img src="/img/bioregistry-ui-august-2025.png" alt="" /></p>

<h3 id="api-and-resolver-usage">API and Resolver Usage</h3>

<p>Many usages of the Bioregistry are in the form of the resolution of links like
<a href="https://bioregistry.io/chembl:CHEMBL4303805">https://bioregistry.io/chembl:CHEMBL4303805</a>.
Under the hood, the Bioregistry can expand prefixes like
<a href="https://bioregistry.io/chembl:CHEMBL4303805"><code class="language-plaintext highlighter-rouge">chembl:CHEMBL4303805</code></a> to URLs.
This makes it a perfect service to support other applications that reference
entities and want to provide external links, without having to maintain the
links themselves.</p>

<p><img src="/img/bioregistry-api-august-2025.png" alt="" /></p>

<p>I’m just getting this working, so it should also be able to better keep track of
unique users (note it’s just showing 1 so far) and their countries. Luckily, all
of this is GDPR-compliant from the beginning.</p>

<h3 id="code-usage">Code Usage</h3>

<p>The Bioregistry distributes a Python package that can be installed with
<code class="language-plaintext highlighter-rouge">pip install bioregistry</code>. I used
<a href="https://github.com/search?q=%22import%20bioregistry%22%20OR%20%22from%20bioregistry%20import%22%20-user%3Acthoyt%20-user%3Asorgerlab%20-user%3Abiopragmatics%20-is%3Afork%20-user%3Apyobo%20-user%3Apybel%20-user%3Agyorilab&amp;type=code">this query</a>
to search GitHub for places where the <code class="language-plaintext highlighter-rouge">bioregistry</code> Python package is imported
and took detailed notes about their context (see details
<a href="https://biopragmatics.github.io/bioregistry/usages">here</a>).</p>

<p>I found a large variety of usages across software packages, databases,
ontologies, knowledge graphs, frameworks, and data models. Because the
<a href="https://linkml.io/">LinkML</a> modeling language’s runtime indirectly depends on
the Bioregistry, there are also dozens to hundreds of projects that indirectly
use it. I also noted some usages from large organizations in the bioinformatics
space, such as SciBite, Synapse, and the NFDI.</p>

<p><a href="https://docs.google.com/drawings/d/1TqjUc2lxgBaAKhOknYL2erxiqcswevVkhf3mu-7jNtE/edit?usp=sharing">
<img src="https://docs.google.com/drawings/d/e/2PACX-1vQmRTV8DxPfj0UL0i1dgkiFTgnhvI3mRQxt-sekL5tWCc8d4DLthv-48oBxXRisCdGUluovp4CXcEgN/pub?w=1440&amp;h=1080" />
</a></p>

<h2 id="indirect-usage">Indirect Usage</h2>

<p>I used Wikidata as a backend to assess the indirect usage of the Bioregistry. I
did this in a few steps:</p>

<ol>
  <li>Make sure all direct usages have a Wikidata item that has a relationship to
the <a href="https://www.wikidata.org/wiki/Q109302681">Bioregistry Wikidata item</a> via
the uses
<a href="https://www.wikidata.org/wiki/Property:P1547">P1547 (depends on software)</a>
or <a href="https://www.wikidata.org/wiki/Property:P2283">P2283 (uses)</a> predicates.</li>
  <li>Identify well-known usages of direct usages, make sure they have Wikidata
items, and are connected to them</li>
  <li>Automate querying Wikidata for all direct and indirect usages</li>
  <li>Search PubMed and quantify mentions of all direct and indirect usages</li>
</ol>

<h3 id="indirect-software-dependencies">Indirect Software Dependencies</h3>

<p>I’ve been developing
<a href="https://github.com/cthoyt/quickstatements_client">quickstatements-client</a> for
automating adding content to Wikidata. I added an extension to it that pulls
metadata from the Python Package Index (PyPI) and adds Python software packages.
It does this recursively for a given package and its dependencies while adding
appropriate
<a href="https://www.wikidata.org/wiki/Property:P1547">P1547 (depends on software)</a>
relations between them.</p>

<p>Unfortunately, this workflow is still limited because it doesn’t find depedent
software. This could be solved by doing a bulk download of PyPI and a
large-scale network analysis. It might also be possible to extract this
information from GitHub. However, for now, this is a good first step.</p>

<h3 id="indirect-ontology-dependencies-via-odk">Indirect Ontology Dependencies via ODK</h3>

<p>The Ontology Development Kit (ODK) uses the Bioregistry in several ways. It is
used by many ontologies both in and out of the OBO Foundry. I
<a href="https://github.com/biopragmatics/bioregistry/pull/1650">developed a workflow</a>
for identifying ODK usage by searching GitHub and iteratively filtering out
false positives. This resulted in over 140 repositories, more than half of which
could directly be mapped back to the Bioregistry, given it tracks the repository
associated with each prefix.</p>

<p>I have previously used
<a href="https://github.com/cthoyt/quickstatements_client">quickstatements-client</a> for
automatically adding records for OBO Foundry ontologies. In next steps, I will
extend this for the other ontologies identified by this analysis and also map
them back to the ODK using the
<a href="https://www.wikidata.org/wiki/Property:P1547">P1547 (depends on software)</a>
relation.</p>

<p>As a side note, for repositories using the ODK that couldn’t be mapped, I
automated making stub curations in the Bioregistry, which lead to the curation
of dozens of new prefixes.</p>

<h3 id="querying-wikidata">Querying Wikidata</h3>

<p>There’s a long tail of different ways to curate indirect usages of the
Bioregistry. However, I believe that the largest cumulative impact for now will
be though the lens of the ontologies built using it. After working through the
scenarios above, I wrote a SPARQL query that recovers all direct and indirect
dependencies (see live table below).</p>

<iframe style="width: 80vw; height: 50vh; border: none;" src="https://query.wikidata.org/embed.html#SELECT%20DISTINCT%0A%3Fitem%20%3FshortName%20%3FitemLabel%20%3FitemDescription%20(GROUP_CONCAT(DISTINCT%20%3Ftype%3B%20separator%3D%22%7C%22)%20as%20%3Ftypes)%0AWHERE%20%7B%0A%20%20VALUES%20%3Fsoftware%20%7B%20wd%3AQ109302681%20wd%3AQ116738064%20%7D%0A%20%20%3Fitem%20(wdt%3AP1547%7Cwdt%3AP2283)%2B%20%3Fsoftware%20.%0A%20%20OPTIONAL%20%7B%20%3Fitem%20wdt%3AP1813%20%3FshortName%20.%20%7D%0A%20%20%3Fitem%20wdt%3AP31%2Frdfs%3Alabel%20%3Ftype%20.%0A%20%20FILTER(lang(%3Ftype)%20%3D%20'en')%0A%20%20SERVICE%20wikibase%3Alabel%20%7B%20bd%3AserviceParam%20wikibase%3Alanguage%20%22%5BAUTO_LANGUAGE%5D%2Cmul%2Cen%22.%20%7D%0A%7D%0AGROUP%20BY%20%3Fitem%20%3FshortName%20%3FitemLabel%20%3FitemDescription%0A" referrerpolicy="origin" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>

<h3 id="literature-analysis">Literature Analysis</h3>

<p>The final step was to analyze the literature for mentions of each of these
direct and indirect dependencies. In typical Charlie fashion, I wanted to
develop high-quality, modular, reusable software for doing this analysis, which
lead to <a href="https://github.com/cthoyt/pubmed-downloader">pubmed-downloader</a> for
wrapping API-based and bulk queries to PubMed (in addition to bulk downloading
and processing, but that’s a story for a different blog post).</p>

<p><img src="https://raw.githubusercontent.com/cthoyt/bioregistry-impact/refs/heads/main/data/output.png" alt="" /></p>

<p>This is pretty great, it shows there are potentially thousands of papers that
mention software, ontologies, databases, etc. that either directly or indirectly
use the Bioregistry. It also shows, unsurprisingly, that there’s a power-law
distribution in which is mentioned most in the literature.</p>

<p>The code for the literature analysis is available in its
<a href="https://github.com/cthoyt/bioregistry-impact">own repository</a>. Of course, there
is still lots of room for improvement and optimization, such as:</p>

<ol>
  <li>Making the image above nicer!</li>
  <li>Using synonyms for search</li>
  <li>Removing false positives</li>
  <li>Incorporating citation networks</li>
</ol>

<p>I’m interested to also make this work flow semi-automated to help assess the
impact of other software/data resources, especially other key software that’s
supporting the OBO Foundry like
<a href="https://github.com/mapping-commons/sssom-py">SSSOM-py</a>.</p>

<hr />

<p>Parting thoughts: it does seem like the Bioregistry is having a meaningful
impact as a relatively young project. However, it’s still an uphill climb to get
more direct recognition and adoption, which requires investing a lot of time in
community building. This isn’t easy for me as an early career researcher not
only because of lack of time/funding but also because of my relative lack of
authority in the community of scientists who would benefit most from the
Bioregistry (which comes with time).</p>

<p>I consider Chris Mungall and his group as the gold standard of being able to
make large impact fast - they are known and trusted in the community, they have
dedicated support staff that can focus on community management, they have many
developers, and many projects where they can push their technologies. I’ll get
there eventually!</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="CURIEs" /><category term="URIs" /><category term="URNs" /><category term="IRIs" /><category term="identifiers" /><category term="identifier schema" /><category term="Bioregistry" /><category term="biopragmatics" /><summary type="html"><![CDATA[The Bioregistry is a database and toolchain for standardization of prefixes, CURIEs, and URIs that appear in linked (open) data. While I created it in 2019 as a component of PyOBO in order to support parsing database cross-references appearing in biomedical ontologies, it has since become an independent project with a community-driven governance model and much broader applications. This post is a first attempt to quantify its usage and impact.]]></summary></entry><entry><title type="html">The Bioregistry and BiomarkerKB</title><link href="https://cthoyt.com/2025/08/22/bioregistry-and-biomarkerkb.html" rel="alternate" type="text/html" title="The Bioregistry and BiomarkerKB" /><published>2025-08-22T08:32:00+00:00</published><updated>2025-08-22T08:32:00+00:00</updated><id>https://cthoyt.com/2025/08/22/bioregistry-and-biomarkerkb</id><content type="html" xml:base="https://cthoyt.com/2025/08/22/bioregistry-and-biomarkerkb.html"><![CDATA[<p>The <a href="https://bioregistry.io">Bioregistry</a> is a community-driven registry of
semantic spaces and their metadata. When I learned about
<a href="https://biomarkerkb.org">BiomarkerKB</a> at the International Society for
Biocuration’s
<a href="https://www.stowers.org/events/biocuration2025">18th Annual International Biocuration Conference</a>,
I was excited to curate new records (and prefixes) in the Bioregistry to cover
BiomarkerKB’s semantic spaces on biomarkers. This post summarizes the
discussions I’ve had with its maintainers, Jeet and Raja, throughout the
Bioregistry curation process and also gives insight into how databases can
benefit from being represented in the Bioregistry.</p>

<h2 id="how-to-contribute-to-the-bioregistry">How to Contribute to the Bioregistry</h2>

<p>The Bioregistry follows the
<a href="https://www.nature.com/articles/s41597-024-03406-w">open data, open code, open infrastructure (O3) guidelines</a>,
meaning that anyone can make suggestions or additions either by creating an
issue or a pull request on its
<a href="https://github.com/biopragmatics/bioregistry">GitHub repository</a>. Both
maintainers of ontologies, controlled vocabularies, databases, etc. and
proactive community members can request new prefixes (i.e., add new records that
represent a semantic space) by following the project’s
<a href="https://github.com/biopragmatics/bioregistry/blob/main/docs/CONTRIBUTING.md#content-contribution">contribution guidelines</a>
(TL;DR, <a href="https://www.youtube.com/watch?v=e-I6rcV2_BE">watch this tutorial</a>).</p>

<h2 id="contributing-new-prefixes-for-biomarkerkb">Contributing New Prefixes for BiomarkerKB</h2>

<p>Most importantly, I coordinated with its maintainers Raja and Jeet to make a
<a href="https://github.com/biopragmatics/bioregistry/pull/1527">pull request</a> to the
Bioregistry’s <a href="https://github.com/biopragmatics/bioregistry">GitHub repository</a>
and add two new prefixes. I was also able to explain the relationship between
the records in the Bioregistry and their resource. I recorded part of our
discussion that is generally useful for anyone who’s making a resource, and want
to think about some of the benefits:</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/Of1mH_uSBpc?si=mPp_3r_9fUgq22Mp" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>As a follow-up, Jeet and Raja requested a summary of this discussion that they
could incorporate into BiomarkerKB’s FAQ. Below is a draft of that text, which
could be easily adapted for any resource.</p>

<h2 id="text-for-biomarkerkbs-faq">Text for BiomarkerKB’s FAQ</h2>

<p>The <a href="https://bioregistry.io">Bioregistry</a> is a community-driven registry of
semantic spaces and their metadata. It contains records for three semantic
spaces created for BiomarkerKB:</p>

<ol>
  <li><a href="https://bioregistry.io/obci"><code class="language-plaintext highlighter-rouge">obci</code></a> for the Ontology for Biomarkers of
Clinical Interest</li>
  <li><a href="https://bioregistry.io/biomarkerkb.biomarker"><code class="language-plaintext highlighter-rouge">biomarkerkb.biomarker</code></a> for
condition-specific biomarkers</li>
  <li><a href="https://bioregistry.io/biomarkerkb.canonical"><code class="language-plaintext highlighter-rouge">biomarkerkb.canonical</code></a> for
condition-agnostic biomarkers</li>
</ol>

<p>The Bioregistry enables BiomarkerKB to unambiguously communicate how its
entities should be written as URIs and compact URIs (CURIEs) for use in semantic
web and linked (open) data settings. This supports many kinds of scientists,
including:</p>

<ul>
  <li><strong>biocurators</strong>, to identify BiomarkerKB’s semantic spaces as high quality
resources for use in annotating their own data</li>
  <li><strong>data providers</strong>, to make their data and knowledge more FAIR before and
during publication</li>
  <li><strong>data scientists</strong>, to validate biomarker data annotated with BiomarkerKB
semantic spaces against an actionable</li>
  <li><strong>data stewards</strong>, to reference the Bioregistry records as components of
research data management plans (DMPs)</li>
  <li><strong>web developers</strong>, to resolve identifiers from BiomarkerKB’s semantic spaces
to human-readable web pages</li>
</ul>

<hr />

<p>If you’re a database maintainer and would like some specific attention given to
representing your resource in the Bioregistry, or explainaning how the
Bioregistry could be used for your project, please reach out! My contact
information is at the bottom of my website.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="biomarker" /><category term="semantic spaces" /><category term="Bioregistry" /><category term="BiomarkerKB" /><summary type="html"><![CDATA[The Bioregistry is a community-driven registry of semantic spaces and their metadata. When I learned about BiomarkerKB at the International Society for Biocuration’s 18th Annual International Biocuration Conference, I was excited to curate new records (and prefixes) in the Bioregistry to cover BiomarkerKB’s semantic spaces on biomarkers. This post summarizes the discussions I’ve had with its maintainers, Jeet and Raja, throughout the Bioregistry curation process and also gives insight into how databases can benefit from being represented in the Bioregistry.]]></summary></entry><entry><title type="html">Text-based embeddings of ontology terms</title><link href="https://cthoyt.com/2025/08/04/ontology-text-embeddings.html" rel="alternate" type="text/html" title="Text-based embeddings of ontology terms" /><published>2025-08-04T08:32:00+00:00</published><updated>2025-08-04T08:32:00+00:00</updated><id>https://cthoyt.com/2025/08/04/ontology-text-embeddings</id><content type="html" xml:base="https://cthoyt.com/2025/08/04/ontology-text-embeddings.html"><![CDATA[<p>The <a href="https://www.ebi.ac.uk/ols4/">Ontology Lookup Service (OLS)</a> is now indexing
dense embeddings for ontology terms constructed from term labels, synonyms, and
descriptions using LLMs. I maintain a Python client library for the OLS
(<a href="https://github.com/cthoyt/ols-client"><code class="language-plaintext highlighter-rouge">ols-client</code></a>) and was recently asked to
implement a wrapper to the OLS’s API endpoint that exposes these embeddings.
This post is a demo of how to use that code, and how I replicated the same
embedding functionality with <a href="https://github.com/biopragmatics/pyobo">PyOBO</a> to
arbitrarily extend it to ontologies and databases not in OLS.</p>

<p>I’ve been <a href="/2025/01/23/clinical-trials-data-modeling.html">working on modeling clinical
trials</a> in
collaboration with <a href="https://github.com/sebastianduesing">Sebastian Duesing</a> at
the <a href="https://bioregistry.io/obi">Ontology for Biomedical Investigations (OBI)</a>,
so I’m going to use the OBI term for
<a href="https://www.ebi.ac.uk/ols4/ontologies/obi/classes/http%253A%252F%252Fpurl.obolibrary.org%252Fobo%252FOBI_0003699">clinical trial (OBI:0003699)</a>
that we recently minted together as an example.</p>

<h2 id="embeddings-from-ols">Embeddings from OLS</h2>

<p>Below is a screenshot of the OLS page for this term, as of August
4<sup>th</sup>, 2025:</p>

<p><img src="/img/ols-clinical-trial-2025-08-04.png" alt="ols-clinical-trial-2025-08-04.png" /></p>

<p>There’s a newly added section on similar terms. Here’s what it says about these:</p>

<blockquote>
  <p>Similarity results are derived from LLM embeddings and have not been manually
curated. Model:
<a href="https://platform.openai.com/docs/models/text-embedding-3-small">text-embedding-3-small</a></p>
</blockquote>

<p>From a first glance, the results here have very high precision. This shouldn’t
be surprising, considering that labels, synonyms, and descriptions contain very
high signal. Of course, there’s room for arguing about the nuance of ontological
differences, but this is a largely unhelpful discussion in my experience when
the goal is to do ontology merging and data integration. In <a href="/2025/01/23/clinical-trials-data-modeling.html">my recent
post</a> where I described
the landscape of clinical trial modeling in the OBO Foundry and related
biomedical ontologies, I actually had already curated several of these semantic
mappings by hand, which I’m hoping to
<a href="https://github.com/obi-ontology/obi/issues/1893">add to OBI via SSSOM</a>. Looking
forward for the OLS, it would be great if there were a mini curation interface
where these could be confirmed or rejected as exact mappings, and persist the
resulting curations as SSSOM.</p>

<p>There was <a href="https://github.com/cthoyt/ols-client/issues/9">a request</a> to expose
the embeddings via my OLS client package, which I solved with only a few lines
of code <a href="https://github.com/cthoyt/ols-client/pull/10/files">here</a>. Now, you can
get the embedding for <em>clinical trial</em> (or any other term, based on the
ontology/IRI combination) as a list of floating point numbers:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">ols_client</span> <span class="kn">import</span> <span class="n">EBIClient</span>

<span class="n">client</span> <span class="o">=</span> <span class="n">EBIClient</span><span class="p">()</span>
<span class="n">embedding</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">]</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">get_embedding</span><span class="p">(</span>
    <span class="s">"obi"</span><span class="p">,</span> <span class="s">"http://purl.obolibrary.org/obo/OBI_0003699"</span>
<span class="p">)</span>
</code></pre></div></div>

<p>The next step is to be able to calculate the (cosine) similarity between two
terms, which can be done between the OBI term for <em>clinical trial</em>, and the
<a href="https://bioregistry.io/ncit">National Cancer Institute Thesaurus (NCIT)</a> term
for clinical trial as a single floating point number:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">ols_client</span> <span class="kn">import</span> <span class="n">EBIClient</span>

<span class="n">client</span> <span class="o">=</span> <span class="n">EBIClient</span><span class="p">()</span>
<span class="n">similarity</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">get_embedding_similarity</span><span class="p">(</span>
    <span class="s">"obi"</span><span class="p">,</span>
    <span class="s">"http://purl.obolibrary.org/obo/OBI_0003699"</span><span class="p">,</span>
    <span class="s">"ncit"</span><span class="p">,</span>
    <span class="s">"http://purl.obolibrary.org/obo/NCIT_C71104"</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<h2 id="embeddings-from-pyobo">Embeddings from PyOBO</h2>

<p>I think using an OpenAI model is a bit overkill for two reasons. First, there
are smaller, non-large language models like
<a href="https://huggingface.co/docs/transformers/en/model_doc/bert">BERT</a> that can get
the same job done. Second, they’re free to download and can be run on commodity
hardware, versus needing to pay OpenAI for access to their embeddings.
Specifically, I’ve been looking at <a href="https://sbert.net">SBERT (Sentence-BERT)</a>,
which is a variant of the BERT architecture that works better on sentences and
can be easily used via the
<a href="https://pypi.org/project/sentence-transformers/"><code class="language-plaintext highlighter-rouge">sentence-transformers</code></a>
Python package.</p>

<p>I’ve developed the <a href="https://github.com/biopragmatics/pyobo"><code class="language-plaintext highlighter-rouge">pyobo</code></a> package,
which gives unified access to ontologies and databases that are ontology-like.
It has functionality for getting the labels, synonyms, and descriptions for
terms in both.</p>

<p>It wasn’t difficult to
<a href="https://github.com/biopragmatics/pyobo/pull/412">re-implement the same functionality as the OLS in PyOBO</a>
such that it can be run locally on a larger variety of resources. Here’s the
same lookup for text embedding and similarity:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pyobo</span>

<span class="o">&gt;&gt;&gt;</span> <span class="n">pyobo</span><span class="p">.</span><span class="n">get_text_embedding</span><span class="p">(</span><span class="s">"OBI:0003699"</span><span class="p">)</span>
<span class="p">[</span><span class="o">-</span><span class="mf">5.68335280e-02</span>  <span class="mf">7.96175096e-03</span> <span class="o">-</span><span class="mf">3.36112119e-02</span>  <span class="mf">2.34440481e-03</span> <span class="p">...</span> <span class="p">]</span>

<span class="o">&gt;&gt;&gt;</span> <span class="n">pyobo</span><span class="p">.</span><span class="n">get_text_embedding_similarity</span><span class="p">(</span><span class="s">"OBI:0003699"</span><span class="p">,</span> <span class="s">"NCIT:C71104"</span><span class="p">)</span>
<span class="mf">0.24702128767967224</span>
</code></pre></div></div>

<p>This could be improved with the ability to do batch lookup, which is probably
the way people would want to use this functionality. Even better, because of how
ML is implemented on GPUs and related hardware, batching effectively comes for
free, only limited by memory contraints.</p>

<hr />

<p>Text embeddings aren’t the end of the story - I’ve been working for several
years on applications fo graph machine learning in biomedical applications. A
lot of the good methodological and software engineering ideas I’ve
had/encountered have gone into the <a href="https://github.com/pykeen/pykeen">PyKEEN</a>
Python software package. One of those ideas that jointly leverages text and
knowledge graph embeddings is <a href="https://arxiv.org/abs/2106.12144">NodePiece</a>, the
work of PyKEEN core developer Michael Galkin. A guide on using it with PyKEEN is
<a href="https://pykeen.readthedocs.io/en/stable/tutorial/inductive_lp.html">here</a>, but
there’s a lot more research to present on this than fits in the postscript of a
blog post or a software tutorial. Along with the last few years of my work on
data integration, I haven’t yet published my magnum opus on biomedical knowledge
graph construction and applications with graph machine learning.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="ontology" /><category term="embeddings" /><category term="bert" /><category term="sbert" /><category term="similarity" /><category term="cosine similarity" /><summary type="html"><![CDATA[The Ontology Lookup Service (OLS) is now indexing dense embeddings for ontology terms constructed from term labels, synonyms, and descriptions using LLMs. I maintain a Python client library for the OLS (ols-client) and was recently asked to implement a wrapper to the OLS’s API endpoint that exposes these embeddings. This post is a demo of how to use that code, and how I replicated the same embedding functionality with PyOBO to arbitrarily extend it to ontologies and databases not in OLS.]]></summary></entry><entry><title type="html">Inference over Semantic Mappings with SeMRA</title><link href="https://cthoyt.com/2025/04/28/inference-with-semra.html" rel="alternate" type="text/html" title="Inference over Semantic Mappings with SeMRA" /><published>2025-04-28T13:17:00+00:00</published><updated>2025-04-28T13:17:00+00:00</updated><id>https://cthoyt.com/2025/04/28/inference-with-semra</id><content type="html" xml:base="https://cthoyt.com/2025/04/28/inference-with-semra.html"><![CDATA[<p>Assembling and inferring missing semantic mappings is a timely problem in
biomedical data and knowledge integration. I’ve been developing the
<a href="https://github.com/biopragmatics/semra">Semantic Mapping Assembler and Reasoner (SeMRA)</a>
as a generic toolkit for this. In this blog post, I highlight its inference
capabilities.</p>

<p>SeMRA implements the chaining and inference rules described in the
<a href="https://mapping-commons.github.io/sssom/chaining-rules/">SSSOM</a> specification.
The first rule is
<a href="https://mapping-commons.github.io/sssom/chaining-rules/#inverse-rules">inversions</a>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">semra</span> <span class="kn">import</span> <span class="n">Mapping</span><span class="p">,</span> <span class="n">EXACT_MATCH</span><span class="p">,</span> <span class="n">Reference</span>
<span class="kn">from</span> <span class="nn">semra.inference</span> <span class="kn">import</span> <span class="n">infer_reversible</span>

<span class="n">r1</span> <span class="o">=</span> <span class="n">Reference</span><span class="p">(</span><span class="n">prefix</span><span class="o">=</span><span class="s">"chebi"</span><span class="p">,</span> <span class="n">identifier</span><span class="o">=</span><span class="s">"107635"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"2,3-diacetyloxybenzoic"</span><span class="p">)</span>
<span class="n">r2</span> <span class="o">=</span> <span class="n">Reference</span><span class="p">(</span><span class="n">prefix</span><span class="o">=</span><span class="s">"mesh"</span><span class="p">,</span> <span class="n">identifier</span><span class="o">=</span><span class="s">"C011748"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"tosiben"</span><span class="p">)</span>

<span class="n">mapping</span> <span class="o">=</span> <span class="n">Mapping</span><span class="p">(</span><span class="n">s</span><span class="o">=</span><span class="n">r1</span><span class="p">,</span> <span class="n">p</span><span class="o">=</span><span class="n">EXACT_MATCH</span><span class="p">,</span> <span class="n">o</span><span class="o">=</span><span class="n">r2</span><span class="p">)</span>

<span class="n">mappings</span> <span class="o">=</span> <span class="n">infer_reversible</span><span class="p">([</span><span class="n">mapping</span><span class="p">])</span>
</code></pre></div></div>

<pre><code class="language-mermaid">graph LR
    A[2,3-diacetyloxybenzoic&lt;br/&gt;chebi:107635] -- skos:exactMatch --&gt; B[tosiben&lt;br/&gt;mesh:C011748]
    B -. "skos:exactMatch&lt;br/&gt;(inferred)" .-&gt; A
</code></pre>

<p>The second rule is about
<a href="https://mapping-commons.github.io/sssom/chaining-rules/#transitivity-rule">transitivity</a>.
This means some predicates apply over chains. SeMRA further implements
configuration for two-length chains and could be extended to arbitrary chains.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">semra</span> <span class="kn">import</span> <span class="n">Reference</span><span class="p">,</span> <span class="n">Mapping</span><span class="p">,</span> <span class="n">EXACT_MATCH</span>
<span class="kn">from</span> <span class="nn">semra.inference</span> <span class="kn">import</span> <span class="n">infer_chains</span>

<span class="n">r1</span> <span class="o">=</span> <span class="n">Reference</span><span class="p">.</span><span class="n">from_curie</span><span class="p">(</span><span class="s">"mesh:C406527"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"R 115866"</span><span class="p">)</span>
<span class="n">r2</span> <span class="o">=</span> <span class="n">Reference</span><span class="p">.</span><span class="n">from_curie</span><span class="p">(</span><span class="s">"chebi:101854"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"talarozole"</span><span class="p">)</span>
<span class="n">r3</span> <span class="o">=</span> <span class="n">Reference</span><span class="p">.</span><span class="n">from_curie</span><span class="p">(</span><span class="s">"chembl.compound:CHEMBL459505"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"TALAROZOLE"</span><span class="p">)</span>

<span class="n">m1</span> <span class="o">=</span> <span class="n">Mapping</span><span class="p">(</span><span class="n">s</span><span class="o">=</span><span class="n">r1</span><span class="p">,</span> <span class="n">p</span><span class="o">=</span><span class="n">EXACT_MATCH</span><span class="p">,</span> <span class="n">o</span><span class="o">=</span><span class="n">r2</span><span class="p">)</span>
<span class="n">m2</span> <span class="o">=</span> <span class="n">Mapping</span><span class="p">(</span><span class="n">s</span><span class="o">=</span><span class="n">r2</span><span class="p">,</span> <span class="n">p</span><span class="o">=</span><span class="n">EXACT_MATCH</span><span class="p">,</span> <span class="n">o</span><span class="o">=</span><span class="n">r3</span><span class="p">)</span>

<span class="n">mappings</span> <span class="o">=</span> <span class="n">infer_chains</span><span class="p">([</span><span class="n">m1</span><span class="p">,</span> <span class="n">m2</span><span class="p">])</span>
</code></pre></div></div>

<pre><code class="language-mermaid">graph LR
    A[R 115866&lt;br/&gt;mesh:C406527] -- skos:exactMatch --&gt; B[talarozole&lt;br/&gt;chebi:101854]
    B -- skos:exactMatch --&gt; C[TALAROZOLE&lt;br/&gt;chembl.compound:CHEMBL459505]
    A -. "skos:exactMatch&lt;br/&gt;(inferred)" .-&gt; C
</code></pre>

<p>The third rule is
<a href="https://mapping-commons.github.io/sssom/chaining-rules/#generalisation-rules">generalization</a>,
which means that a more strict predicate can be relaxed to a less specific
predicate, like <code class="language-plaintext highlighter-rouge">owl:equivalentTo</code> to <code class="language-plaintext highlighter-rouge">skos:exactMatch</code>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">semra</span> <span class="kn">import</span> <span class="n">Reference</span><span class="p">,</span> <span class="n">Mapping</span><span class="p">,</span> <span class="n">EXACT_MATCH</span><span class="p">,</span> <span class="n">EQUIVALENT_TO</span>
<span class="kn">from</span> <span class="nn">semra.inference</span> <span class="kn">import</span> <span class="n">infer_generalizations</span>

<span class="n">r1</span> <span class="o">=</span> <span class="n">Reference</span><span class="p">.</span><span class="n">from_curie</span><span class="p">(</span><span class="s">"chebi:101854"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"talarozole"</span><span class="p">)</span>
<span class="n">r2</span> <span class="o">=</span> <span class="n">Reference</span><span class="p">.</span><span class="n">from_curie</span><span class="p">(</span><span class="s">"chembl.compound:CHEMBL459505"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"TALAROZOLE"</span><span class="p">)</span>

<span class="n">m1</span> <span class="o">=</span> <span class="n">Mapping</span><span class="p">(</span><span class="n">s</span><span class="o">=</span><span class="n">r1</span><span class="p">,</span> <span class="n">p</span><span class="o">=</span><span class="n">EXACT_MATCH</span><span class="p">,</span> <span class="n">o</span><span class="o">=</span><span class="n">r2</span><span class="p">)</span>

<span class="n">mappings</span> <span class="o">=</span> <span class="n">infer_generalizations</span><span class="p">([</span><span class="n">m1</span><span class="p">])</span>
</code></pre></div></div>

<pre><code class="language-mermaid">graph LR
    A[talarozole&lt;br/&gt;chebi:101854] -- owl:equivalentTo --&gt; B[TALAROZOLE&lt;br/&gt;chembl.compound:CHEMBL459505]
    A -. "skos:exactMatch&lt;br/&gt;(inferred)" .-&gt; B
</code></pre>

<p>The third rule can actually be generalized to any kinds of mutation of one
predicate to another, given some domain knowledge. For example, some resources
curate <code class="language-plaintext highlighter-rouge">oboInOwl:hasDbXref</code> predicates when it’s implied that they mean
<code class="language-plaintext highlighter-rouge">skos:exactMatch</code> because the resource is curated in the OBO flat file format.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">semra</span> <span class="kn">import</span> <span class="n">Reference</span><span class="p">,</span> <span class="n">Mapping</span><span class="p">,</span> <span class="n">DB_XREF</span>
<span class="kn">from</span> <span class="nn">semra.inference</span> <span class="kn">import</span> <span class="n">infer_dbxref_mutations</span>

<span class="n">r1</span> <span class="o">=</span> <span class="n">Reference</span><span class="p">.</span><span class="n">from_curie</span><span class="p">(</span><span class="s">"doid:0050577"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"cranioectodermal dysplasia"</span><span class="p">)</span>
<span class="n">r2</span> <span class="o">=</span> <span class="n">Reference</span><span class="p">.</span><span class="n">from_curie</span><span class="p">(</span><span class="s">"mesh:C562966"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"Cranioectodermal Dysplasia"</span><span class="p">)</span>
<span class="n">m1</span> <span class="o">=</span> <span class="n">Mapping</span><span class="p">(</span><span class="n">s</span><span class="o">=</span><span class="n">r1</span><span class="p">,</span> <span class="n">p</span><span class="o">=</span><span class="n">DB_XREF</span><span class="p">,</span> <span class="n">o</span><span class="o">=</span><span class="n">r2</span><span class="p">)</span>

<span class="c1"># we're 99% confident doid-mesh dbxrefs actually are exact matches
</span><span class="n">mappings</span> <span class="o">=</span> <span class="n">infer_dbxref_mutations</span><span class="p">([</span><span class="n">m1</span><span class="p">],</span> <span class="p">{(</span><span class="s">"doid"</span><span class="p">,</span> <span class="s">"mesh"</span><span class="p">):</span> <span class="mf">0.99</span><span class="p">})</span>
</code></pre></div></div>

<pre><code class="language-mermaid">graph LR
    A[cranioectodermal dysplasia&lt;br/&gt;doid:0050577] -- oboInOwl:hasDbXref --&gt; B[Cranioectodermal Dysplasia&lt;br/&gt;mesh:C562966]
    A -. "skos:exactMatch&lt;br/&gt;(inferred)" .-&gt; B
</code></pre>

<hr />

<p>There’s a lot more to say about semantic mappings - a good first place to look
before getting into the guts of the code is the accompanying manuscript:</p>

<blockquote>
  <p><a href="https://www.biorxiv.org/content/10.1101/2025.04.16.649126">Assembly
and reasoning over semantic mappings at scale for biomedical data
integration</a><br />Hoyt, C. T., Karis K., and Gyori, B. M.<br /><em>bioRxiv</em>,
2025.04.16.649126</p>
</blockquote>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="ontology merging" /><category term="semantic web" /><category term="semantic mappings" /><category term="bioinformatics" /><category term="ontologies" /><category term="controlled vocabularies" /><summary type="html"><![CDATA[Assembling and inferring missing semantic mappings is a timely problem in biomedical data and knowledge integration. I’ve been developing the Semantic Mapping Assembler and Reasoner (SeMRA) as a generic toolkit for this. In this blog post, I highlight its inference capabilities.]]></summary></entry><entry><title type="html">I wish I could unpack Callables in Python type annotations</title><link href="https://cthoyt.com/2025/04/23/python-wish-unpacking-callable.html" rel="alternate" type="text/html" title="I wish I could unpack Callables in Python type annotations" /><published>2025-04-23T13:23:00+00:00</published><updated>2025-04-23T13:23:00+00:00</updated><id>https://cthoyt.com/2025/04/23/python-wish-unpacking-callable</id><content type="html" xml:base="https://cthoyt.com/2025/04/23/python-wish-unpacking-callable.html"><![CDATA[<p>Following the theme of my previous two posts, I’ve run into another typing
conundrum where I want to unpack a pre-existing <code class="language-plaintext highlighter-rouge">Callable</code> into a class with
<code class="language-plaintext highlighter-rouge">Generic[P, T]</code> where P is a parameter specification type (i.e. <code class="language-plaintext highlighter-rouge">ParamsSpec</code>)</p>

<p>After figuring out the right way to <a href="/2025/04/22/python-generic-with-paramspec.html">declare a generic featuring a
<code class="language-plaintext highlighter-rouge">ParamSpec</code></a>, I updated
the <a href="https://github.com/cthoyt/class-resolver"><code class="language-plaintext highlighter-rouge">class-resolver</code></a> package to use
the shiny new (and more accurate) annotations. Unfortunately, reality set in,
and within hours, someone
<a href="https://github.com/pykeen/pykeen/issues/1539">reported this caused errors</a> in
<a href="https://github.com/pykeen/pykeen">PyKEEN</a>, my graph machine learning software
package that heavily uses <code class="language-plaintext highlighter-rouge">class-resolver</code> to make its knowledge graph embedding
models modular and configurable.</p>

<p>I was able to fix PyKEEN, but the fact that I was previously using a named type
alias for <code class="language-plaintext highlighter-rouge">Normalizer: TypeHint = Callable[[torch.Tensor], torch.Tensor]</code> and
then had to re-write it yet again the variable declaration’s type hint as
<code class="language-plaintext highlighter-rouge">FunctionResolver[[torch.Tensor], torch.Tensor]</code> was not great. What I would
love is some way to unpack <code class="language-plaintext highlighter-rouge">Normalizer</code> into the arguments of
<code class="language-plaintext highlighter-rouge">FunctionResolver[...]</code>.</p>

<p>Let me demonstrate in a more self-contained way:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Callable</span><span class="p">,</span> <span class="n">Generic</span><span class="p">,</span> <span class="n">Unpack</span><span class="p">,</span> <span class="n">ParamSpec</span><span class="p">,</span> <span class="n">TypeVar</span>

<span class="n">P</span> <span class="o">=</span> <span class="n">ParamSpec</span><span class="p">(</span><span class="s">"P"</span><span class="p">)</span>
<span class="n">T</span> <span class="o">=</span> <span class="n">TypeVar</span><span class="p">(</span><span class="s">"T"</span><span class="p">)</span>

<span class="k">class</span> <span class="nc">Box</span><span class="p">(</span><span class="n">Generic</span><span class="p">[</span><span class="n">P</span><span class="p">,</span> <span class="n">T</span><span class="p">]):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">func</span><span class="p">:</span> <span class="n">Callable</span><span class="p">[</span><span class="n">P</span><span class="p">,</span> <span class="n">T</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">func</span> <span class="o">=</span> <span class="n">func</span>

<span class="k">def</span> <span class="nf">f</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="k">return</span> <span class="nb">str</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>

<span class="c1"># This works!
</span><span class="n">box_1</span><span class="p">:</span> <span class="n">Box</span><span class="p">[[</span><span class="nb">int</span><span class="p">],</span> <span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">Box</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>

<span class="n">FType</span> <span class="o">=</span> <span class="n">Callable</span><span class="p">[[</span><span class="nb">int</span><span class="p">],</span> <span class="nb">str</span><span class="p">]</span>

<span class="c1"># I wish I could do this, in case I already
# had a type variable referring to Callable[[int], str]
</span><span class="n">box_2</span><span class="p">:</span> <span class="n">Box</span><span class="p">[</span><span class="n">Unpack</span><span class="p">[</span><span class="n">FType</span><span class="p">]]</span> <span class="o">=</span> <span class="n">Box</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
</code></pre></div></div>

<p>While I’m abusing the usage of
<a href="https://docs.python.org/3/library/typing.html#typing.Unpack"><code class="language-plaintext highlighter-rouge">typing.Unpack</code></a>,
some way of extracting the arguments of one type and splatting them into another
seems like it <em>might</em> have a use case. Maybe.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="Python" /><category term="mypy" /><category term="static typing" /><summary type="html"><![CDATA[Following the theme of my previous two posts, I’ve run into another typing conundrum where I want to unpack a pre-existing Callable into a class with Generic[P, T] where P is a parameter specification type (i.e. ParamsSpec)]]></summary></entry><entry><title type="html">Using ParamSpec with Python Generics</title><link href="https://cthoyt.com/2025/04/22/python-generic-with-paramspec.html" rel="alternate" type="text/html" title="Using ParamSpec with Python Generics" /><published>2025-04-22T08:31:00+00:00</published><updated>2025-04-22T08:31:00+00:00</updated><id>https://cthoyt.com/2025/04/22/python-generic-with-paramspec</id><content type="html" xml:base="https://cthoyt.com/2025/04/22/python-generic-with-paramspec.html"><![CDATA[<p>I’ve been working on applying strict static typing to my Python package
<a href="https://github.com/cthoyt/class-resolver"><code class="language-plaintext highlighter-rouge">class-resolver</code></a> and ran into an
interesting way of using
<a href="https://docs.python.org/3/library/typing.html#generics">generics</a> in
combination with parameter specification variables (i.e.,
<a href="https://docs.python.org/3/library/typing.html#typing.ParamSpec">ParamSpec</a>s).</p>

<p>Normally, if you want to type annotate a function, you use the
<a href="https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable"><code class="language-plaintext highlighter-rouge">Callable</code></a>,
which works like the following:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Callable</span>

<span class="c1">#: the [int] represents a function that takes in a single integer,
#:  and returns a single floating point number
</span><span class="n">IntToFloat</span> <span class="o">=</span> <span class="n">Callable</span><span class="p">[[</span><span class="nb">int</span><span class="p">],</span> <span class="nb">float</span><span class="p">]</span>

<span class="c1"># this function fits the type annotation above, impl omitted
</span><span class="k">def</span> <span class="nf">square_root</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="p">...</span>

<span class="c1"># simple example to show how to write functions that consume functions
</span><span class="k">def</span> <span class="nf">applies_int_to_float</span><span class="p">(</span><span class="n">func</span><span class="p">:</span> <span class="n">Callable</span><span class="p">[[</span><span class="nb">int</span><span class="p">],</span> <span class="nb">float</span><span class="p">],</span> <span class="n">x</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">func</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>


<span class="o">&gt;&gt;&gt;</span> <span class="n">applies_int_to_float</span><span class="p">(</span><span class="n">square_root</span><span class="p">,</span> <span class="mi">9</span><span class="p">)</span>
<span class="mf">3.0</span>
</code></pre></div></div>

<p>However, if you want to get generic, you need to use a combination of
<a href="https://docs.python.org/3/library/typing.html#typing.ParamSpec">ParamSpec</a> for
the input variable signature and
<a href="https://docs.python.org/3/library/typing.html#typing.TypeVar">TypeVar</a> for the
return value.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Callable</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">TypeVar</span>

<span class="n">X</span> <span class="o">=</span> <span class="n">TypeVar</span><span class="p">(</span><span class="s">"X"</span><span class="p">)</span>
<span class="n">T</span> <span class="o">=</span> <span class="n">TypeVar</span><span class="p">(</span><span class="s">"T"</span><span class="p">)</span>

<span class="c1"># simple example to show how to write functions that consume functions
</span><span class="k">def</span> <span class="nf">applies_unary_function</span><span class="p">(</span><span class="n">func</span><span class="p">:</span> <span class="n">Callable</span><span class="p">[[</span><span class="n">X</span><span class="p">],</span> <span class="n">T</span><span class="p">],</span> <span class="n">x</span><span class="p">:</span> <span class="n">X</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">T</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">func</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>


<span class="o">&gt;&gt;&gt;</span> <span class="n">applies_unary_function</span><span class="p">(</span><span class="n">square_root</span><span class="p">,</span> <span class="mi">9</span><span class="p">)</span>
<span class="mf">3.0</span>
</code></pre></div></div>

<p>If you want to make the input fully generic, you can use <code class="language-plaintext highlighter-rouge">ParamSpec</code> and
reference <code class="language-plaintext highlighter-rouge">P.args</code> (or <code class="language-plaintext highlighter-rouge">P.kwargs</code>, not shown here):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Callable</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">ParamSpec</span><span class="p">,</span> <span class="n">TypeVar</span>

<span class="n">P</span> <span class="o">=</span> <span class="n">ParamSpec</span><span class="p">(</span><span class="s">"P"</span><span class="p">)</span>
<span class="n">T</span> <span class="o">=</span> <span class="n">TypeVar</span><span class="p">(</span><span class="s">"T"</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">applies_unary_function_generic</span><span class="p">(</span><span class="n">func</span><span class="p">:</span> <span class="n">Callable</span><span class="p">[</span><span class="n">P</span><span class="p">,</span> <span class="n">T</span><span class="p">],</span> <span class="o">*</span><span class="n">args</span><span class="p">:</span> <span class="n">P</span><span class="p">.</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">:</span> <span class="n">P</span><span class="p">.</span><span class="n">kwargs</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">T</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">func</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>


<span class="o">&gt;&gt;&gt;</span> <span class="n">applies_unary_function_generic</span><span class="p">(</span><span class="n">square_root</span><span class="p">,</span> <span class="mi">9</span><span class="p">)</span>
<span class="mf">3.0</span>
</code></pre></div></div>

<p>This gets even a bit more complicated if you want to type annotate a class that
can take in a generic set of functions. Here’s an example on how this works:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Callable</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Generic</span><span class="p">,</span> <span class="n">ParamSpec</span><span class="p">,</span> <span class="n">TypeVar</span>

<span class="n">P</span> <span class="o">=</span> <span class="n">ParamSpec</span><span class="p">(</span><span class="s">"P"</span><span class="p">)</span>
<span class="n">T</span> <span class="o">=</span> <span class="n">TypeVar</span><span class="p">(</span><span class="s">"T"</span><span class="p">)</span>


<span class="k">class</span> <span class="nc">ListOfFunctions</span><span class="p">(</span><span class="n">Generic</span><span class="p">[</span><span class="n">P</span><span class="p">,</span> <span class="n">T</span><span class="p">]):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">functions</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Callable</span><span class="p">[</span><span class="n">P</span><span class="p">,</span> <span class="n">T</span><span class="p">]])</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">functions</span> <span class="o">=</span> <span class="n">functions</span>


<span class="k">def</span> <span class="nf">identity</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">x</span>


<span class="k">def</span> <span class="nf">plus_two</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">2</span>

<span class="n">my_list</span><span class="p">:</span> <span class="n">ListOfFunctions</span><span class="p">[[</span><span class="nb">int</span><span class="p">],</span> <span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">ListOfFunctions</span><span class="p">([</span><span class="n">identity</span><span class="p">,</span> <span class="n">plus_two</span><span class="p">])</span>
</code></pre></div></div>

<p>The most important part of this discovery for me was actually how to
type-annotate the resulting object, which magically is able to accept a list in
the place where the <code class="language-plaintext highlighter-rouge">ParamSpec</code> should be.</p>

<p>Here’s the new way to write the same class using
<a href="https://peps.python.org/pep-0695/">PEP-0695</a> type parameter syntax, introduced
in Python 3.12:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Callable</span>


<span class="k">class</span> <span class="nc">ListOfFunctions</span><span class="p">[</span><span class="o">**</span><span class="n">P</span><span class="p">,</span> <span class="n">T</span><span class="p">]:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">functions</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Callable</span><span class="p">[</span><span class="n">P</span><span class="p">,</span> <span class="n">T</span><span class="p">]])</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">functions</span> <span class="o">=</span> <span class="n">functions</span>
</code></pre></div></div>

<hr />

<p>My two big wishes for typing in the future:</p>

<ul>
  <li>make the builtin <code class="language-plaintext highlighter-rouge">any</code> be a valid substitution for <code class="language-plaintext highlighter-rouge">typing.Any</code></li>
  <li>make some cute syntax, so I don’t need to import
<code class="language-plaintext highlighter-rouge">from collections.abc import Callable</code></li>
</ul>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="Python" /><category term="mypy" /><category term="static typing" /><summary type="html"><![CDATA[I’ve been working on applying strict static typing to my Python package class-resolver and ran into an interesting way of using generics in combination with parameter specification variables (i.e., ParamSpecs).]]></summary></entry><entry><title type="html">A dilemma with PEP-696 default generics when using optional static typing in Python</title><link href="https://cthoyt.com/2025/04/19/python-default-typing-dilemma.html" rel="alternate" type="text/html" title="A dilemma with PEP-696 default generics when using optional static typing in Python" /><published>2025-04-19T15:45:00+00:00</published><updated>2025-04-19T15:45:00+00:00</updated><id>https://cthoyt.com/2025/04/19/python-default-typing-dilemma</id><content type="html" xml:base="https://cthoyt.com/2025/04/19/python-default-typing-dilemma.html"><![CDATA[<p>This post describes an issue I’ve had with writing correct types when using
<a href="https://peps.python.org/pep-0696">PEP-696</a> defaults in <code class="language-plaintext highlighter-rouge">typing.TypeVar</code>. I
posted the exploration in a
<a href="https://github.com/cthoyt/python-typing-dilemma">companion repository</a> on
GitHub.</p>

<p>The motivation behind this comes from my work in biomedical data integration and
the semantic web. I wrote the <a href="https://github.com/cthoyt/curies"><code class="language-plaintext highlighter-rouge">curies</code></a>
Python package to provide a fully generic and reusable data model for
representing pairs of prefixes and local unique identifiers via the
<a href="https://curies.readthedocs.io/en/latest/api/curies.Reference.html"><code class="language-plaintext highlighter-rouge">curies.Reference</code></a>
class.</p>

<p>I extended this class in the
<a href="https://github.com/biopragmatics/bioregistry"><code class="language-plaintext highlighter-rouge">bioregistry</code></a> in order to
validate and standardize prefixes and local unique identifiers using its
detailed set of metadata rules. This is implemented in the
<a href="https://bioregistry.readthedocs.io/en/stable/api/bioregistry.Registry.html"> <code class="language-plaintext highlighter-rouge">bioregistry.NormalizedReference</code></a>.</p>

<p>In other places, like <a href="https://github.com/cthoyt/ssslm"><code class="language-plaintext highlighter-rouge">ssslm</code></a>, I’ve built on
the <code class="language-plaintext highlighter-rouge">curies.Reference</code> data structure for maximum compatibility. However,
sometimes I want to be able to inject the <code class="language-plaintext highlighter-rouge">bioregistry.Reference</code> class to get
the guarantees of standardization. However, I don’t want the <code class="language-plaintext highlighter-rouge">ssslm</code> package to
have to know about the Bioregistry, since I want to keep <code class="language-plaintext highlighter-rouge">ssslm</code> package
generic.</p>

<p>The solution might be with <a href="https://peps.python.org/pep-0696">PEP-0696</a>, which
extends the ability to specify generic types with defaults. This means places
where I used to hard-code a <code class="language-plaintext highlighter-rouge">curie.Reference</code>, I can now use a generic on the
entire class that has <code class="language-plaintext highlighter-rouge">curie.Reference</code> as the default… in theory.</p>

<p>In practice, it isn’t so easy. I boiled it down to a fully self-contained
example (which I also dumped in
<a href="https://github.com/cthoyt/python-typing-dilemma">https://github.com/cthoyt/python-typing-dilemma</a>).</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Any</span><span class="p">,</span> <span class="n">TypeVar</span>

<span class="nb">type</span> <span class="n">Record</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]</span>


<span class="k">class</span> <span class="nc">Element</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">record</span><span class="p">:</span> <span class="n">Record</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">record</span> <span class="o">=</span> <span class="n">record</span>


<span class="k">class</span> <span class="nc">DerivedElement</span><span class="p">(</span><span class="n">Element</span><span class="p">):</span>
    <span class="k">pass</span>


<span class="c1"># note, we're using PEP-696 default keyword, which is available from Python 3.13 onwards
</span><span class="n">T</span> <span class="o">=</span> <span class="n">TypeVar</span><span class="p">(</span><span class="s">"T"</span><span class="p">,</span> <span class="n">bound</span><span class="o">=</span><span class="n">Element</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="n">Element</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">from_record_1</span><span class="p">(</span><span class="n">record</span><span class="p">:</span> <span class="n">Record</span><span class="p">,</span> <span class="n">element_cls</span><span class="p">:</span> <span class="nb">type</span><span class="p">[</span><span class="n">T</span><span class="p">]</span> <span class="o">=</span> <span class="n">Element</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">T</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">element_cls</span><span class="p">(</span><span class="n">record</span><span class="p">)</span>
</code></pre></div></div>

<p>This <em>should</em> work. In fact, MyPy is able to infer the types correctly for the
return value. The big question is: what’s the correct way to type-annotate
<code class="language-plaintext highlighter-rouge">element_cls</code>? If you run MyPy on this, you get (abridged output):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>git clone https://github.com/cthoyt/python-typing-dilemma
<span class="gp">$</span><span class="w"> </span><span class="nb">cd </span>python-typing-dilemma
<span class="gp">$</span><span class="w"> </span>uvx <span class="nt">--python</span> 3.13 mypy <span class="nt">--strict</span> main.py
<span class="go">main.py:23: error: Incompatible default for argument "element_cls" (default has type "type[Element]", argument has type "type[T]")  [assignment]
</span></code></pre></div></div>

<p>I tried a few other things:</p>

<ul>
  <li>Defining a second type variable
<code class="language-plaintext highlighter-rouge">TType = TypeVar("TType", bound=type[Element])</code> or
<code class="language-plaintext highlighter-rouge">TType = TypeVar("TType", bound=type[Element], default=type[Element])</code> (not
included in the repo)</li>
  <li>Defining a second type based on the first
<code class="language-plaintext highlighter-rouge">TType = TypeVar("TType", bound=type[T])</code> (not included in the repo)</li>
  <li>Using <code class="language-plaintext highlighter-rouge">None</code> as a sentinel value (try #2)</li>
  <li>Using overloads (try #3, suggested by Guido)</li>
</ul>

<p>None of this worked, so I asked for help. Turns out, other people ran into this
issue already and brought it up with MyPy.</p>

<ul>
  <li>https://github.com/python/mypy/issues/3737</li>
  <li>https://github.com/python/mypy/issues/12962</li>
  <li>https://github.com/python/mypy/issues/18812</li>
</ul>

<p>Right now, I don’t think my use case can be solved, so I’ll have to sit tight!</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="Python" /><category term="mypy" /><category term="static typing" /><summary type="html"><![CDATA[This post describes an issue I’ve had with writing correct types when using PEP-696 defaults in typing.TypeVar. I posted the exploration in a companion repository on GitHub.]]></summary></entry><entry><title type="html">The EFO_ID column in ChEMBL’s drug indications table isn’t what you think it is</title><link href="https://cthoyt.com/2025/04/17/chembl-indications-efo-exploration.html" rel="alternate" type="text/html" title="The EFO_ID column in ChEMBL’s drug indications table isn’t what you think it is" /><published>2025-04-17T16:42:00+00:00</published><updated>2025-04-17T16:42:00+00:00</updated><id>https://cthoyt.com/2025/04/17/chembl-indications-efo-exploration</id><content type="html" xml:base="https://cthoyt.com/2025/04/17/chembl-indications-efo-exploration.html"><![CDATA[<p>ChEMBL periodically curates clinical trial information into its
<code class="language-plaintext highlighter-rouge">DRUG_INDICATION</code> table. However, there’s some weird inconsistencies in the way
it references disease concepts in external vocabularies. This blog post is an
exploration of that table.</p>

<p>As of ChEMBL v35, the <code class="language-plaintext highlighter-rouge">DRUG_INDICATION</code> table contains the following columns:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">DRUGIND_ID</code> - a unique identifier for the chemical-indication pair</li>
  <li><code class="language-plaintext highlighter-rouge">MOLREGNO</code> - a foreign key to the molecules table</li>
  <li><code class="language-plaintext highlighter-rouge">MAX_PHASE_FOR_IND</code> - the maximum phase achieved by clinical trials of the
chemical-indication pair</li>
  <li><code class="language-plaintext highlighter-rouge">MESH_ID</code> - the local unique identifier from
<a href="https://bioregistry.io/mesh">Medical Subject Headings (MeSH)</a> for the
indication</li>
  <li><code class="language-plaintext highlighter-rouge">MESH_HEADING</code> - the label in MeSH for the given MeSH ID</li>
  <li><code class="language-plaintext highlighter-rouge">EFO_ID</code> - A compact URI (CURIE) for a term in the
<a href="https://bioregistry.io/efo">Experimental Factor Ontology (EFO)</a> ( in theory)</li>
  <li><code class="language-plaintext highlighter-rouge">EFO_TERM</code> - a label for the CURIE in the <code class="language-plaintext highlighter-rouge">EFO_ID</code> column</li>
</ol>

<p>This is already strange, considering that for cell lines, tissues, and targets,
ChEMBL has created its own table which contains the cross-references to external
vocabularies. Here, they’re baked into the pivot table.</p>

<p>The funny business is about the <code class="language-plaintext highlighter-rouge">EFO_ID</code> column:</p>

<ol>
  <li>It’s strange that the <code class="language-plaintext highlighter-rouge">MESH_ID</code> column uses local unique identifiers but the
<code class="language-plaintext highlighter-rouge">EFO_ID</code> column uses compact URIs (CURIEs). CURIEs are a syntax for
referencing an entity in an ontology or database that takes the form of
<code class="language-plaintext highlighter-rouge">&lt;prefix&gt;:&lt;local unique identifier&gt;</code>. The <code class="language-plaintext highlighter-rouge">prefix</code> is usually the acronym for
the resource and the local unique identifier is the ID inside the resource
(usually a number). More on this in <a href="/2021/09/14/curies.html">my previous
post</a>.</li>
  <li>The CURIEs in the <code class="language-plaintext highlighter-rouge">EFO_ID</code> column aren’t all using <code class="language-plaintext highlighter-rouge">EFO</code> as the prefix!</li>
</ol>

<p>Let’s have a look at what’s actually in the <code class="language-plaintext highlighter-rouge">EFO_ID</code> column by using the
<a href="https://github.com/cthoyt/chembl-downloader"><code class="language-plaintext highlighter-rouge">chembl_downloader</code></a> Python
package to automatically download the latest version of ChEMBL and run SQL
queries over it.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">chembl_downloader</span>

<span class="n">chembl_downloader</span><span class="p">.</span><span class="n">query</span><span class="p">(</span><span class="s">"""</span><span class="se">\
</span><span class="s">   SELECT DISTINCT efo_id, efo_term
   FROM DRUG_INDICATION
   WHERE efo_id NOT LIKE 'EFO:%'
"""</span><span class="p">)</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>efo_id</th>
      <th>efo_term</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>HP:0001945</td>
      <td>Fever</td>
    </tr>
    <tr>
      <td>Orphanet:309005</td>
      <td>Disorder of lipid metabolism</td>
    </tr>
    <tr>
      <td>HP:0003124</td>
      <td>Hypercholesterolemia</td>
    </tr>
    <tr>
      <td>Orphanet:79211</td>
      <td>Combined hyperlipidemia</td>
    </tr>
    <tr>
      <td>HP:0000023</td>
      <td>Inguinal hernia</td>
    </tr>
    <tr>
      <td>…</td>
      <td>…</td>
    </tr>
  </tbody>
</table>

<p>Using a bit of SQL string processing to identify the prefixes and the
<a href="https://bioregistry.io">Bioregistry</a> to retrieve the name and homepage gives a
bit more context about what the prefixes in CURIEs in the <code class="language-plaintext highlighter-rouge">EFO_ID</code> column
represent.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">bioregistry</span>
<span class="kn">import</span> <span class="nn">chembl_downloader</span>

<span class="n">sql</span> <span class="o">=</span> <span class="s">"""</span><span class="se">\
</span><span class="s">SELECT prefix, count(prefix) as count
FROM (
    SELECT substr(efo_id, 0, instr(efo_id, ":")) as prefix
    FROM DRUG_INDICATION
)
GROUP BY prefix
HAVING count(prefix) &gt; 0
ORDER BY count(prefix) DESC
"""</span>

<span class="n">df</span> <span class="o">=</span> <span class="n">chembl_downloader</span><span class="p">.</span><span class="n">query</span><span class="p">(</span><span class="n">sql</span><span class="p">)</span>
<span class="n">df</span><span class="p">[</span><span class="s">"name"</span><span class="p">]</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="s">"prefix"</span><span class="p">].</span><span class="nb">map</span><span class="p">(</span><span class="n">bioregistry</span><span class="p">.</span><span class="n">get_name</span><span class="p">)</span>
<span class="n">df</span><span class="p">[</span><span class="s">"homepage"</span><span class="p">]</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="s">"prefix"</span><span class="p">].</span><span class="nb">map</span><span class="p">(</span><span class="n">bioregistry</span><span class="p">.</span><span class="n">get_homepage</span><span class="p">)</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>prefix</th>
      <th style="text-align: right">count</th>
      <th>name</th>
      <th>homepage</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>EFO</td>
      <td style="text-align: right">37,603</td>
      <td>Experimental Factor Ontology</td>
      <td>http://www.ebi.ac.uk/efo</td>
    </tr>
    <tr>
      <td>MONDO</td>
      <td style="text-align: right">13,532</td>
      <td>Mondo Disease Ontology</td>
      <td>https://monarch-initiative.github.io/mondo</td>
    </tr>
    <tr>
      <td>HP</td>
      <td style="text-align: right">3,381</td>
      <td>Human Phenotype Ontology</td>
      <td>http://www.human-phenotype-ontology.org/</td>
    </tr>
    <tr>
      <td>Orphanet</td>
      <td style="text-align: right">359</td>
      <td>Orphanet</td>
      <td>http://www.orpha.net/consor/</td>
    </tr>
    <tr>
      <td>MP</td>
      <td style="text-align: right">281</td>
      <td>Mammalian Phenotype Ontology</td>
      <td>https://www.informatics.jax.org/vocab/mp_ontology/</td>
    </tr>
    <tr>
      <td>GO</td>
      <td style="text-align: right">50</td>
      <td>Gene Ontology</td>
      <td>http://geneontology.org/</td>
    </tr>
    <tr>
      <td>DOID</td>
      <td style="text-align: right">45</td>
      <td>Human Disease Ontology</td>
      <td>http://www.disease-ontology.org</td>
    </tr>
    <tr>
      <td>CHEBI</td>
      <td style="text-align: right">19</td>
      <td>Chemical Entities of Biological Interest</td>
      <td>http://www.ebi.ac.uk/chebi</td>
    </tr>
    <tr>
      <td>UBERON</td>
      <td style="text-align: right">1</td>
      <td>Uber Anatomy Ontology</td>
      <td>http://uberon.org</td>
    </tr>
  </tbody>
</table>

<p>The ones that stand out to me are <code class="language-plaintext highlighter-rouge">CHEBI</code>, <code class="language-plaintext highlighter-rouge">UBERON</code>, and <code class="language-plaintext highlighter-rouge">GO</code>, since these
resources are respectively for chemicals, anatomical entities, and biological
processes/cellular components/molecular functions.</p>

<p>I wrote the following function to do a bit of exploring, based on the prefix.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">chembl_downloader</span>

<span class="k">def</span> <span class="nf">print_indications_with_prefix</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
    <span class="n">sql</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"""</span><span class="se">\
</span><span class="s">        SELECT DISTINCT
            MOLECULE_DICTIONARY.chembl_id,
            MOLECULE_DICTIONARY.pref_name,
            DRUG_INDICATION.efo_id,
            DRUG_INDICATION.efo_term
        FROM MOLECULE_DICTIONARY
            JOIN DRUG_INDICATION ON MOLECULE_DICTIONARY.molregno == DRUG_INDICATION.molregno
        WHERE DRUG_INDICATION.efo_id LIKE '</span><span class="si">{</span><span class="n">prefix</span><span class="si">}</span><span class="s">:%'
        ORDER BY MOLECULE_DICTIONARY.pref_name
        """</span>
    <span class="n">df</span> <span class="o">=</span> <span class="n">chembl_downloader</span><span class="p">.</span><span class="n">query</span><span class="p">(</span><span class="n">sql</span><span class="p">)</span>
    <span class="n">df</span><span class="p">[</span><span class="s">"chembl_id"</span><span class="p">]</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="s">"chembl_id"</span><span class="p">].</span><span class="nb">map</span><span class="p">(</span><span class="k">lambda</span> <span class="n">s</span><span class="p">:</span> <span class="sa">f</span><span class="s">"[</span><span class="si">{</span><span class="n">s</span><span class="si">}</span><span class="s">](https://bioregistry.io/chembl.compound:</span><span class="si">{</span><span class="n">s</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>
    <span class="n">df</span><span class="p">[</span><span class="s">"efo_id"</span><span class="p">]</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="s">"efo_id"</span><span class="p">].</span><span class="nb">map</span><span class="p">(</span><span class="k">lambda</span> <span class="n">s</span><span class="p">:</span> <span class="sa">f</span><span class="s">"[</span><span class="si">{</span><span class="n">s</span><span class="si">}</span><span class="s">](https://bioregistry.io/</span><span class="si">{</span><span class="n">s</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="n">df</span><span class="p">.</span><span class="n">to_markdown</span><span class="p">(</span><span class="n">tablefmt</span><span class="o">=</span><span class="s">"github"</span><span class="p">,</span> <span class="n">index</span><span class="o">=</span><span class="bp">False</span><span class="p">))</span>
</code></pre></div></div>

<p>Using <code class="language-plaintext highlighter-rouge">UBERON</code> returns a single result, which appears to be a mistake / an abuse
of the database schema.</p>

<table>
  <thead>
    <tr>
      <th>chembl_id</th>
      <th>pref_name</th>
      <th>efo_id</th>
      <th>efo_term</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL4650497">CHEMBL4650497</a></td>
      <td>PEGSITACIANINE</td>
      <td><a href="https://bioregistry.io/UBERON:0000029">UBERON:0000029</a></td>
      <td>lymph node</td>
    </tr>
  </tbody>
</table>

<p>Using <code class="language-plaintext highlighter-rouge">CHEBI</code> returns a large number of diagnostic agents. This is part of the
“role” hierarchy within ChEBI, and also what I would consider an abuse of the
database schema.</p>

<table>
  <thead>
    <tr>
      <th>chembl_id</th>
      <th>pref_name</th>
      <th>efo_id</th>
      <th>efo_term</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL1234270">CHEMBL1234270</a></td>
      <td>ARFOLITIXORIN</td>
      <td><a href="https://bioregistry.io/CHEBI:44185">CHEBI:44185</a></td>
      <td>methotrexate</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL5314823">CHEMBL5314823</a></td>
      <td>DIGADOGLUCITOL</td>
      <td><a href="https://bioregistry.io/CHEBI:33295">CHEBI:33295</a></td>
      <td>diagnostic agent</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL4650354">CHEMBL4650354</a></td>
      <td>FLORBENGUANE F18</td>
      <td><a href="https://bioregistry.io/CHEBI:33295">CHEBI:33295</a></td>
      <td>diagnostic agent</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL5095045">CHEMBL5095045</a></td>
      <td>FLORZOLOTAU (18F)</td>
      <td><a href="https://bioregistry.io/CHEBI:33295">CHEBI:33295</a></td>
      <td>diagnostic agent</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL5314559">CHEMBL5314559</a></td>
      <td>FLOTUFOLASTAT F 18 GALLIUM</td>
      <td><a href="https://bioregistry.io/CHEBI:33295">CHEBI:33295</a></td>
      <td>diagnostic agent</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL4298157">CHEMBL4298157</a></td>
      <td>FLUBROBENGUANE F18</td>
      <td><a href="https://bioregistry.io/CHEBI:33295">CHEBI:33295</a></td>
      <td>diagnostic agent</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL5314633">CHEMBL5314633</a></td>
      <td>IODINE I124 EVUZAMITIDE</td>
      <td><a href="https://bioregistry.io/CHEBI:33295">CHEBI:33295</a></td>
      <td>diagnostic agent</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL4298185">CHEMBL4298185</a></td>
      <td>LONAPEGSOMATROPIN</td>
      <td><a href="https://bioregistry.io/CHEBI:37845">CHEBI:37845</a></td>
      <td>growth hormone</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL5314761">CHEMBL5314761</a></td>
      <td>PEGFOSIMER MANGANESE</td>
      <td><a href="https://bioregistry.io/CHEBI:33295">CHEBI:33295</a></td>
      <td>diagnostic agent</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL4650497">CHEMBL4650497</a></td>
      <td>PEGSITACIANINE</td>
      <td><a href="https://bioregistry.io/CHEBI:33295">CHEBI:33295</a></td>
      <td>diagnostic agent</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL4297334">CHEMBL4297334</a></td>
      <td>PIFLUFOLASTAT F18</td>
      <td><a href="https://bioregistry.io/CHEBI:33295">CHEBI:33295</a></td>
      <td>diagnostic agent</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL5314483">CHEMBL5314483</a></td>
      <td>RIZEDISBEN</td>
      <td><a href="https://bioregistry.io/CHEBI:33295">CHEBI:33295</a></td>
      <td>diagnostic agent</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL5314650">CHEMBL5314650</a></td>
      <td>TECHNETIUM TC-99M LABELED CARBON</td>
      <td><a href="https://bioregistry.io/CHEBI:33295">CHEBI:33295</a></td>
      <td>diagnostic agent</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL4298067">CHEMBL4298067</a></td>
      <td>TOMARALIMAB</td>
      <td><a href="https://bioregistry.io/CHEBI:35610">CHEBI:35610</a></td>
      <td>antineoplastic agent</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL5314445">CHEMBL5314445</a></td>
      <td>VIDOFLUFOLASTAT(18F)</td>
      <td><a href="https://bioregistry.io/CHEBI:33295">CHEBI:33295</a></td>
      <td>diagnostic agent</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL4594280">CHEMBL4594280</a></td>
      <td>VIPIVOTIDE TETRAXETAN</td>
      <td><a href="https://bioregistry.io/CHEBI:33295">CHEBI:33295</a></td>
      <td>diagnostic agent</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL4594411">CHEMBL4594411</a></td>
      <td>XENON XE-129, HYPERPOLARIZED</td>
      <td><a href="https://bioregistry.io/CHEBI:33295">CHEBI:33295</a></td>
      <td>diagnostic agent</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL5314610">CHEMBL5314610</a></td>
      <td>ZOPOCIANINE</td>
      <td><a href="https://bioregistry.io/CHEBI:33295">CHEBI:33295</a></td>
      <td>diagnostic agent</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL5314611">CHEMBL5314611</a></td>
      <td>ZOPOCIANINE SODIUM</td>
      <td><a href="https://bioregistry.io/CHEBI:33295">CHEBI:33295</a></td>
      <td>diagnostic agent</td>
    </tr>
  </tbody>
</table>

<p>Using <code class="language-plaintext highlighter-rouge">GO</code> returns aging, regulation of ovulation (both positive and negative),
and wound healing as the four unique biological processes. This is a little less
controversial than UBERON and CHEBI, but it still has a bit of a mismatch for
the idea of an “indication”.</p>

<table>
  <thead>
    <tr>
      <th>chembl_id</th>
      <th>pref_name</th>
      <th>efo_id</th>
      <th>efo_term</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL1566">CHEMBL1566</a></td>
      <td>ACARBOSE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL600">CHEMBL600</a></td>
      <td>ACETYLCYSTEINE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL1399">CHEMBL1399</a></td>
      <td>ANASTROZOLE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL25">CHEMBL25</a></td>
      <td>ASPIRIN</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL1201556">CHEMBL1201556</a></td>
      <td>BECAPLERMIN</td>
      <td><a href="https://bioregistry.io/GO:0042060">GO:0042060</a></td>
      <td>wound healing</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL5315086">CHEMBL5315086</a></td>
      <td>BETULA PUBESCENS BARK</td>
      <td><a href="https://bioregistry.io/GO:0042060">GO:0042060</a></td>
      <td>wound healing</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL1200800">CHEMBL1200800</a></td>
      <td>CALCIUM ACETATE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL1200539">CHEMBL1200539</a></td>
      <td>CALCIUM CARBONATE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL113313">CHEMBL113313</a></td>
      <td>CAPROMORELIN</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL1042">CHEMBL1042</a></td>
      <td>CHOLECALCIFEROL</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL2108185">CHEMBL2108185</a></td>
      <td>CORIFOLLITROPIN ALFA</td>
      <td><a href="https://bioregistry.io/GO:0060279">GO:0060279</a></td>
      <td>positive regulation of ovulation</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL429910">CHEMBL429910</a></td>
      <td>DAPAGLIFLOZIN</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL1421">CHEMBL1421</a></td>
      <td>DASATINIB</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL139">CHEMBL139</a></td>
      <td>DICLOFENAC</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL367149">CHEMBL367149</a></td>
      <td>DOCONEXENT</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL1200969">CHEMBL1200969</a></td>
      <td>DUTASTERIDE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL135">CHEMBL135</a></td>
      <td>ESTRADIOL</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL2108390">CHEMBL2108390</a></td>
      <td>FIBRIN</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL500468">CHEMBL500468</a></td>
      <td>GHRELIN</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL389621">CHEMBL389621</a></td>
      <td>HYDROCORTISONE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL13817">CHEMBL13817</a></td>
      <td>IBUTAMOREN</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL460026">CHEMBL460026</a></td>
      <td>ICOSAPENT</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL2109042">CHEMBL2109042</a></td>
      <td>INFLUENZA VIRUS VACCINE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL471737">CHEMBL471737</a></td>
      <td>IVABRADINE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL444814">CHEMBL444814</a></td>
      <td>L-CITRULLINE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL191">CHEMBL191</a></td>
      <td>LOSARTAN</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL1201419">CHEMBL1201419</a></td>
      <td>LUTROPIN ALFA</td>
      <td><a href="https://bioregistry.io/GO:0060279">GO:0060279</a></td>
      <td>positive regulation of ovulation</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL2107951">CHEMBL2107951</a></td>
      <td>MALTODEXTRIN</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL267936">CHEMBL267936</a></td>
      <td>MECAMYLAMINE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL1201716">CHEMBL1201716</a></td>
      <td>MECASERMIN</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL1431">CHEMBL1431</a></td>
      <td>METFORMIN</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL650">CHEMBL650</a></td>
      <td>METHYLPREDNISOLONE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL4074884">CHEMBL4074884</a></td>
      <td>MITOQUINONE MESYLATE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL438497">CHEMBL438497</a></td>
      <td>NICOTINAMIDE RIBOSIDE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL3">CHEMBL3</a></td>
      <td>NICOTINE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL1201574">CHEMBL1201574</a></td>
      <td>ONABOTULINUMTOXINA</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL1234886">CHEMBL1234886</a></td>
      <td>OXYGEN</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL395429">CHEMBL395429</a></td>
      <td>OXYTOCIN</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL90593">CHEMBL90593</a></td>
      <td>PRASTERONE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL50">CHEMBL50</a></td>
      <td>QUERCETIN</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL165">CHEMBL165</a></td>
      <td>RESVERATROL</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL413">CHEMBL413</a></td>
      <td>SIROLIMUS</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL1200574">CHEMBL1200574</a></td>
      <td>SODIUM CHLORIDE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL93268">CHEMBL93268</a></td>
      <td>SODIUM NITRITE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL136478">CHEMBL136478</a></td>
      <td>SODIUM NITROPRUSSIDE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL1201621">CHEMBL1201621</a></td>
      <td>SOMATROPIN</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL48802">CHEMBL48802</a></td>
      <td>SULFORAPHANE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL386630">CHEMBL386630</a></td>
      <td>TESTOSTERONE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL2107067">CHEMBL2107067</a></td>
      <td>TESTOSTERONE UNDECANOATE</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/chembl.compound:CHEMBL3545347">CHEMBL3545347</a></td>
      <td>TXA127</td>
      <td><a href="https://bioregistry.io/GO:0007568">GO:0007568</a></td>
      <td>aging</td>
    </tr>
  </tbody>
</table>

<hr />

<p>There wasn’t really a point to this post other than to show off a quirk I found
in ChEMBL. This is useful to be aware of when automatically processing the
database in bulk, e.g., for building a knowledge graph.</p>

<p>There are two other follow-up questions I would have about this table:</p>

<ol>
  <li>Are there any EFO terms that are outside the disease hierarchy (i.e., not a
child of <a href="http://www.ebi.ac.uk/efo/EFO_0000408">EFO:0000408</a>)?</li>
  <li>Why are there DOID terms? The combination of EFO and MONDO <em>should</em> cover
everything. Answering this question actually isn’t so difficult given my
recent work on assembling mappings with
<a href="https://github.com/biopragmatics/semra">SeMRA</a>, specifically for the
<a href="https://github.com/biopragmatics/semra/tree/main/notebooks/landscape#example">disease landscape</a>.
I’ll try to come back to this in a future post.</li>
</ol>

<p>If you made it this far: what did you think about my clickbait title?</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="ChEBI" /><category term="ChEMBL" /><category term="UBERON" /><category term="Experimental Factor Ontology" /><category term="EFO" /><category term="GO" /><category term="Gene Ontology" /><category term="clinical trials" /><summary type="html"><![CDATA[ChEMBL periodically curates clinical trial information into its DRUG_INDICATION table. However, there’s some weird inconsistencies in the way it references disease concepts in external vocabularies. This blog post is an exploration of that table.]]></summary></entry><entry><title type="html">Data Modeling and Integration with Clinical Trials</title><link href="https://cthoyt.com/2025/01/23/clinical-trials-data-modeling.html" rel="alternate" type="text/html" title="Data Modeling and Integration with Clinical Trials" /><published>2025-01-23T18:23:00+00:00</published><updated>2025-01-23T18:23:00+00:00</updated><id>https://cthoyt.com/2025/01/23/clinical-trials-data-modeling</id><content type="html" xml:base="https://cthoyt.com/2025/01/23/clinical-trials-data-modeling.html"><![CDATA[<p>I’ve recently worked with clinical studies from
<a href="https://clinicaltrials.gov">ClinicalTrials.gov</a> and
<a href="https://bioregistry.io/collection/0000012">other international registries</a>.
This post is a review on how to access data, a proposal for how it can be
modeled using the
<a href="https://obi-ontology.org/">Ontology for Biomedical Investigations (OBI)</a>, a
<a href="https://w3id.org/biopragmatics/resources/clinicaltrials">proof-of-concept ontologization</a>
of ClinicalTrials.gov, and some insights into how this data can be integrated
with other resources to address classical problems in drug discovery from a
knowledge graph perspective.</p>

<p>This is a long read, so here’s a table of contents:</p>

<ol>
  <li><a href="#automated-download">Automated download of ClinicalTrails.gov with <code class="language-plaintext highlighter-rouge">clinicaltrials-downloader</code></a></li>
  <li><a href="#summarization">Summarizing ClincialTrials.gov study types, allocations, and phases</a></li>
  <li><a href="#example-clinical-studies">Example clinical studies</a></li>
  <li><a href="#proposing-an-ontology-meta-model">Proposing an ontology meta-model</a></li>
  <li><a href="#proof-of-concept-ontology-export-of-clinicaltrialsgov">Proof-of-concept ontology export of ClinicalTrials.gov using <code class="language-plaintext highlighter-rouge">pyobo</code></a></li>
  <li><a href="#reflections-and-whats-missing">Reflections and what’s missing</a></li>
  <li><a href="#whats-this-all-useful-for-anyway">What’s this all useful for, anyway?</a></li>
</ol>

<h2 id="automated-download">Automated Download</h2>

<p>Similar to <a href="/2021/08/05/taming-chembl-sql.html">ChEMBL</a>,
<a href="/2020/12/14/taming-drugbank.html">DrugBank</a>, and
<a href="/2023/09/01/umls.html">UMLS</a>, I authored a Python package, that
automates download and caching clinical studies from
<a href="https://clinicaltrials.gov">ClinicalTrials.gov</a>. Its source code is available
under the MIT license at
<a href="https://github.com/cthoyt/clinicaltrials-downloader">https://github.com/cthoyt/clinicaltrials-downloader</a>,
and it can be installed with <code class="language-plaintext highlighter-rouge">pip install clinicaltrials-downloader</code>. The
package exposes two main functions for getting the raw, unprocessed data:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">clinicaltrials_downloader</span> <span class="kn">import</span> <span class="n">get_studies</span><span class="p">,</span> <span class="n">get_studies_slim</span>

<span class="c1"># contains all fields, (~2GB, gzipped)
</span><span class="n">studies</span> <span class="o">=</span> <span class="n">get_studies</span><span class="p">()</span>

<span class="c1"># contains a useful subset of the fields, much smaller (~70MB, gzipped)
</span><span class="n">studies_slim</span> <span class="o">=</span> <span class="n">get_studies_slim</span><span class="p">()</span>
</code></pre></div></div>

<p>Keep in mind that the data is updated daily, so the caches become out of date
quickly. Pass <code class="language-plaintext highlighter-rouge">force=True</code> to either of the downloader functions to update the
local cache of the database.</p>

<h2 id="summarization">Summarization</h2>

<p>I generated a few summary tables over the slim subset of ClinicalTrials.gov
using
<a href="https://gist.github.com/cthoyt/12a3cb3c63ad68d73fe5a2f0d506526f">this script</a>.
The tables can be regenerated with the following command, adding <code class="language-plaintext highlighter-rouge">--force</code> if
you want to refresh the cache:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>uv run <span class="nt">--script</span> https://gist.githubusercontent.com/cthoyt/12a3cb3c63ad68d73fe5a2f0d506526f/raw/98bd3795ec02ebd2c8bb8746e3a3a5d23aeffd75/clinicaltrials-summary.py
</code></pre></div></div>

<p>As an aside, if you’re not yet familiar with
<a href="https://peps.python.org/pep-0723/">PEP-723</a>, you can now put the requirements
and other metadata inside a script. This also allows me to tell <code class="language-plaintext highlighter-rouge">uv</code> to use the
development version of PyOBO and my fork of Gilda that doesn’t include all its
heavy requirements.</p>

<h3 id="study-type-and-allocation">Study Type and Allocation</h3>

<p>ClinicalTrials.gov contains three main study types:</p>

<ol>
  <li><a href="https://clinicaltrials.gov/study-basics/glossary#interventional-study-clinical-trial">interventional study</a>
(i.e., clinical trial) - a study in which participants are assigned zero or
more diagnostic, therapeutic, or other types of interventions depending on
the arm into which they are allocated</li>
  <li><a href="https://clinicaltrials.gov/study-basics/glossary#observational-study">observational study</a> -
a study in which participants are assessed for biomedical or health outcomes.
They may receive interventions, but they are not assigned like in
interventional studies</li>
  <li><a href="https://clinicaltrials.gov/study-basics/glossary#expanded-access">expanded access</a>
(i.e., compassionate use) - a mechanism through which patients who are not
participants in a clinical trial to receive access to
non-approved/experimental medicine.</li>
</ol>

<p>Interventional studies can be divided into two categories based on their
<a href="https://clinicaltrials.gov/study-basics/glossary#allocation">allocation</a> - the
used to assign participants to an arm of a clinical study. They are
<a href="https://clinicaltrials.gov/study-basics/glossary#randomized-allocation">randomized</a>
and non-randomized.</p>

<p>The table below adjusts the internal labels for legibility, aggregates missing
values and <code class="language-plaintext highlighter-rouge">NA</code> entries, and sorts by most common.</p>

<table>
  <thead>
    <tr>
      <th>Study Type</th>
      <th>Allocation</th>
      <th style="text-align: right">Count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Interventional</td>
      <td>Randomized</td>
      <td style="text-align: right">261,643</td>
    </tr>
    <tr>
      <td>Observational</td>
      <td> </td>
      <td style="text-align: right">120,775</td>
    </tr>
    <tr>
      <td>Interventional</td>
      <td> </td>
      <td style="text-align: right">95,249</td>
    </tr>
    <tr>
      <td>Interventional</td>
      <td>Non-Randomized</td>
      <td style="text-align: right">42,759</td>
    </tr>
    <tr>
      <td>Expanded Access</td>
      <td> </td>
      <td style="text-align: right">966</td>
    </tr>
    <tr>
      <td> </td>
      <td> </td>
      <td style="text-align: right">902</td>
    </tr>
  </tbody>
</table>

<h3 id="clinical-trial-phases">Clinical Trial Phases</h3>

<p>The <a href="https://clinicaltrials.gov/study-basics/glossary#phase">phase</a> primarily
communicates the objective of a clinical trial (i.e., interventional study).
Observational trials and expanded access studies therefore do not have phases.</p>

<p><img src="https://www.hepb.org/assets/Uploads/_resampled/ResizedImageWzk2OSwzNzBd-Clinical-Trial-Process-FlowChart.png" alt="" /></p>

<p>Image above from the
<a href="https://www.hepb.org/research-and-programs/hepdeltaconnect/clinical-trials/">Hepatitis B Foundation</a>.</p>

<p>There are six common phases appearing on ClinicalTrials.gov:</p>

<ul>
  <li><a href="https://clinicaltrials.gov/study-basics/glossary#early-phase-1-formerly-listed-as-phase-0">Early Phase 1</a>
(formerly, Phase 0) - Assess oral bioavailability, pharmacokinetics (very
small group; almost always left out of diagrams like the one above). This is
not the same thing as pre-clinical trials, which are often done with
biochemical assays, cellular assays, and work with model organisms.</li>
  <li><a href="https://clinicaltrials.gov/study-basics/glossary#phase-1">Phase 1</a> - Assess
safety in healthy volunteers (small group)</li>
  <li><a href="https://clinicaltrials.gov/study-basics/glossary#phase-2">Phase 2</a> - Assess
efficacy and side effects (medium group)</li>
  <li><a href="https://clinicaltrials.gov/study-basics/glossary#phase-3">Phase 3</a> - Assess
efficacy, effectiveness, and safety ( large group)</li>
  <li><a href="https://clinicaltrials.gov/study-basics/glossary#phase-4">Phase 4</a> -
Post-approval surveillance</li>
  <li><a href="https://clinicaltrials.gov/study-basics/glossary#phase-not-applicable">Phase Not Applicable (N/A)</a> -
Applied to trials without phases, such as trials with devices or behavioral
interventions</li>
</ul>

<p>The table below adjusts the internal clinical trial phases’ labels for
legibility, aggregates missing values and <code class="language-plaintext highlighter-rouge">NA</code> entries, and sorts by
progression.</p>

<table>
  <thead>
    <tr>
      <th>Phase</th>
      <th style="text-align: right">Count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td style="text-align: right">5,434</td>
    </tr>
    <tr>
      <td>1</td>
      <td style="text-align: right">44,195</td>
    </tr>
    <tr>
      <td>1, 2</td>
      <td style="text-align: right">15,219</td>
    </tr>
    <tr>
      <td>2</td>
      <td style="text-align: right">59,412</td>
    </tr>
    <tr>
      <td>2, 3</td>
      <td style="text-align: right">6,982</td>
    </tr>
    <tr>
      <td>3</td>
      <td style="text-align: right">39,160</td>
    </tr>
    <tr>
      <td>4</td>
      <td style="text-align: right">33,129</td>
    </tr>
    <tr>
      <td>N/A or missing</td>
      <td style="text-align: right">318,763</td>
    </tr>
  </tbody>
</table>

<p>Some trials are annotated with multiple phases, either 1/2 or 2/3. These could
also be combined in a different way for “maximum clinical phase” aggregation
operations.</p>

<p>Unsurprisingly, there is an attrition through the progression of phases, but it
is not as stark as I would have expected. It might also be interesting to
stratify this by year to see if trials are more likely to succeed as time goes
on.</p>

<h2 id="example-clinical-studies">Example Clinical Studies</h2>

<p>Having examples and doing spot-checks is always helpful when exploring new data,
so I generated a table containing example clinical trials for each study type,
allocation, and phase. While there are many studies with more than one
intervention and/or condition, this table only shows trials with a single one of
each to reduce complexity.</p>

<table>
  <thead>
    <tr>
      <th>Study/Phase(s)</th>
      <th>NCT ID</th>
      <th>Title</th>
      <th>Condition</th>
      <th>Intervention</th>
      <th>Structure</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Expanded Access</td>
      <td><a href="https://bioregistry.io/clinicaltrials:NCT01317953">NCT01317953</a></td>
      <td>Oral Green Tea Extract for Small Cell Lung Cancer</td>
      <td><a href="https://bioregistry.io/mesh:D055752">Small Cell Lung Carcinoma</a></td>
      <td><a href="https://bioregistry.io/chebi:4806">(-)-epigallocatechin 3-gallate</a></td>
      <td><img src="https://bioregistry.io/chebi:4806?provider=chebi-img" alt="" /></td>
    </tr>
    <tr>
      <td>Observational</td>
      <td><a href="https://bioregistry.io/clinicaltrials:NCT03418987">NCT03418987</a></td>
      <td>The Vertebral Vector in a Horizontal Plane. A Simple Way to See in 3D.</td>
      <td><a href="https://bioregistry.io/mesh:D012600">Scoliosis</a></td>
      <td> </td>
      <td> </td>
    </tr>
    <tr>
      <td>Non-Randomized (Phase 0)</td>
      <td><a href="https://bioregistry.io/clinicaltrials:NCT01209533">NCT01209533</a></td>
      <td>Inhaled Iloprost in Mild Asthma</td>
      <td><a href="https://bioregistry.io/mesh:D001249">Asthma</a></td>
      <td><a href="https://bioregistry.io/chebi:63916">iloprost</a></td>
      <td><img src="https://bioregistry.io/chebi:63916?provider=chebi-img" alt="" /></td>
    </tr>
    <tr>
      <td>Non-Randomized (Phase 1)</td>
      <td><a href="https://bioregistry.io/clinicaltrials:NCT01682187">NCT01682187</a></td>
      <td>A Dose-Escalation Study in Participants With Recurrent Malignant Glioma</td>
      <td><a href="https://bioregistry.io/mesh:D005910">Glioma</a></td>
      <td><a href="https://bioregistry.io/chebi:6520">lomustine</a></td>
      <td><img src="https://bioregistry.io/chebi:6520?provider=chebi-img" alt="" /></td>
    </tr>
    <tr>
      <td>Non-Randomized (Phase 1/2)</td>
      <td><a href="https://bioregistry.io/clinicaltrials:NCT00379587">NCT00379587</a></td>
      <td>Rituximab for Prevention of Chronic GVHD</td>
      <td><a href="https://bioregistry.io/mesh:D019337">Hematologic Neoplasms</a></td>
      <td><a href="https://bioregistry.io/chebi:64357">rituximab</a></td>
      <td><img src="https://bioregistry.io/chebi:64357?provider=chebi-img" alt="" /></td>
    </tr>
    <tr>
      <td>Non-Randomized (Phase 2)</td>
      <td><a href="https://bioregistry.io/clinicaltrials:NCT00176787">NCT00176787</a></td>
      <td>Radiation Therapy With Capecitabine in Rectal Cancer</td>
      <td><a href="https://bioregistry.io/mesh:D012004">Rectal Neoplasms</a></td>
      <td><a href="https://bioregistry.io/chebi:31348">capecitabine</a></td>
      <td><img src="https://bioregistry.io/chebi:31348?provider=chebi-img" alt="" /></td>
    </tr>
    <tr>
      <td>Non-Randomized (Phase 2/3)</td>
      <td><a href="https://bioregistry.io/clinicaltrials:NCT04431453">NCT04431453</a></td>
      <td>Study of Remdesivir in Participants Below 18 Years Old With COVID-19</td>
      <td><a href="https://bioregistry.io/mesh:D000086382">COVID-19</a></td>
      <td><a href="https://bioregistry.io/chebi:145994">remdesivir</a></td>
      <td><img src="https://bioregistry.io/chebi:145994?provider=chebi-img" alt="" /></td>
    </tr>
    <tr>
      <td>Non-Randomized (Phase 3)</td>
      <td><a href="https://bioregistry.io/clinicaltrials:NCT03184987">NCT03184987</a></td>
      <td>A Long-term Safety Study of Fixed Dose Combination Therapy Fluticasone […]</td>
      <td><a href="https://bioregistry.io/mesh:D001249">Asthma</a></td>
      <td><a href="https://bioregistry.io/chebi:2549">albuterol</a></td>
      <td><img src="https://bioregistry.io/chebi:2549?provider=chebi-img" alt="" /></td>
    </tr>
    <tr>
      <td>Non-Randomized (Phase 4)</td>
      <td><a href="https://bioregistry.io/clinicaltrials:NCT03282487">NCT03282487</a></td>
      <td>Optimising Steroid Replacement in Patients With Adrenal Insufficiency</td>
      <td><a href="https://bioregistry.io/mesh:D000309">Adrenal Insufficiency</a></td>
      <td><a href="https://bioregistry.io/chebi:17650">cortisol</a></td>
      <td><img src="https://bioregistry.io/chebi:17650?provider=chebi-img" alt="" /></td>
    </tr>
    <tr>
      <td>Randomized (Phase 0)</td>
      <td><a href="https://bioregistry.io/clinicaltrials:NCT04293887">NCT04293887</a></td>
      <td>Efficacy and Safety of IFN-α2β in the Treatment of Novel Coronavirus Patients</td>
      <td><a href="https://bioregistry.io/mesh:D018352">Coronavirus Infections</a></td>
      <td><a href="https://bioregistry.io/chebi:52999">interferon</a></td>
      <td><img src="https://bioregistry.io/chebi:52999?provider=chebi-img" alt="" /></td>
    </tr>
    <tr>
      <td>Randomized (Phase 1)</td>
      <td><a href="https://bioregistry.io/clinicaltrials:NCT01166087">NCT01166087</a></td>
      <td>Bioequivalence Study of Fluoxetine Hydrochloride Delayed-Release Capsules […]</td>
      <td><a href="https://bioregistry.io/mesh:D044342">Malnutrition</a></td>
      <td><a href="https://bioregistry.io/chebi:5118">fluoxetine</a></td>
      <td><img src="https://bioregistry.io/chebi:5118?provider=chebi-img" alt="" /></td>
    </tr>
    <tr>
      <td>Randomized (Phase 1/2)</td>
      <td><a href="https://bioregistry.io/clinicaltrials:NCT00106587">NCT00106587</a></td>
      <td>Treatment of In-Stent Restenosis by Paclitaxel Coated PTCA Balloons […]</td>
      <td><a href="https://bioregistry.io/mesh:D023903">Coronary Restenosis</a></td>
      <td><a href="https://bioregistry.io/chebi:45863">paclitaxel</a></td>
      <td><img src="https://bioregistry.io/chebi:45863?provider=chebi-img" alt="" /></td>
    </tr>
    <tr>
      <td>Randomized (Phase 2)</td>
      <td><a href="https://bioregistry.io/clinicaltrials:NCT00094887">NCT00094887</a></td>
      <td>Nitric Oxide Inhalation to Treat Sickle Cell Pain Crises</td>
      <td><a href="https://bioregistry.io/mesh:D000755">Anemia, Sickle Cell</a></td>
      <td><a href="https://bioregistry.io/chebi:16480">nitric oxide</a></td>
      <td><img src="https://bioregistry.io/chebi:16480?provider=chebi-img" alt="" /></td>
    </tr>
    <tr>
      <td>Randomized (Phase 2/3)</td>
      <td><a href="https://bioregistry.io/clinicaltrials:NCT00136487">NCT00136487</a></td>
      <td>Celecoxib (Celebrex) Versus Placebo in Men With Recurrent Prostate Cancer</td>
      <td><a href="https://bioregistry.io/mesh:D011471">Prostatic Neoplasms</a></td>
      <td><a href="https://bioregistry.io/chebi:41423">celecoxib</a></td>
      <td><img src="https://bioregistry.io/chebi:41423?provider=chebi-img" alt="" /></td>
    </tr>
    <tr>
      <td>Randomized (Phase 3)</td>
      <td><a href="https://bioregistry.io/clinicaltrials:NCT00843687">NCT00843687</a></td>
      <td>A Comparison of the Pharmacokinetics and Safety of Long-acting Injectable […]</td>
      <td><a href="https://bioregistry.io/mesh:D012559">Schizophrenia</a></td>
      <td><a href="https://bioregistry.io/chebi:8871">risperidone</a></td>
      <td><img src="https://bioregistry.io/chebi:8871?provider=chebi-img" alt="" /></td>
    </tr>
    <tr>
      <td>Randomized (Phase 4)</td>
      <td><a href="https://bioregistry.io/clinicaltrials:NCT03586687">NCT03586687</a></td>
      <td>Osteoarthritis Shoulder Injection Study</td>
      <td><a href="https://bioregistry.io/mesh:D010003">Osteoarthritis</a></td>
      <td><a href="https://bioregistry.io/chebi:9667">triamcinolone</a></td>
      <td><img src="https://bioregistry.io/chebi:9667?provider=chebi-img" alt="" /></td>
    </tr>
  </tbody>
</table>

<h2 id="proposing-an-ontology-meta-model">Proposing an Ontology Meta-model</h2>

<p>Given my goal to create an ontology export of ClinicalTrials.gov, I had to start
by making some modeling decisions. The first was that each clinical study in the
resource is an <em>instance</em>. This meant that I had to start by finding the right
<em>class</em> for each, corresponding to the study types and allocations that I
explored above.</p>

<h3 id="searching-for-existing-ontology-classes">Searching for Existing Ontology Classes</h3>

<p>The <a href="https://obi-ontology.org">Ontology for Biomedical Investigations (OBI)</a> is
a high quality ontology that contains terms for assays, devices, objectives, and
other aspects of biomedical investigations. Therefore, I would ideally be able
to find terms corresponding to the study types and allocations that I explored
above already inside it.</p>

<p>I used the
<a href="https://www.ebi.ac.uk/ols4/ontologies">Ontology Lookup Service (OLS)</a> to search
for classes corresponding to the study types I explored above, but didn’t find
anything specific enough in OBI. However, I did note that <em>if</em> there would be
classes for clinical studies, then they would appear under its high-level class
for <a href="https://bioregistry.io/obi:0000066">investigation (OBI:0000066)</a></p>

<p>While searching in the OLS, I did find the following relevant classes, each with
their own issues:</p>

<ol>
  <li>The <a href="https://bioregistry.io/sio">Semantic Science Integrated Ontology (SIO)</a>
has a term for clinical trial
(<a href="https://www.ebi.ac.uk/ols4/ontologies/sio/classes/http%253A%252F%252Fsemanticscience.org%252Fresource%252FSIO_001000">SIO:001000</a>),
but SIO doesn’t follow best practices from
<a href="https://obofoundry.org">Open Biological and Biomedical Ontology (OBO) Foundry</a>,
meaning that it is difficult to reuse and less trustworthy.</li>
  <li>The
<a href="https://bioregistry.io/opmi">Ontology of Precision Medicine and Investigation (OPMI)</a>
has a term for clinical trial
(<a href="https://www.ebi.ac.uk/ols4/ontologies/opmi/classes/http%253A%252F%252Fpurl.obolibrary.org%252Fobo%252FOPMI_0004507?lang=en">OPMI:0004507</a>).
I’m not comfortable reusing terms from this ontology for two main reasons:
    <ol>
      <li>It’s use-case specific, and curated based on project-based needs, which
means that it’s not a reliable resource.</li>
      <li>It’s not curated using modern ontology infrastructure, so I’m not sure
that I can trust it will be maintained.</li>
    </ol>
  </li>
  <li>The <a href="https://bioregistry.io/ico">Informed Consent Ontology (ICO)</a> also has a
term for clinical trial
(<a href="https://www.ebi.ac.uk/ols4/ontologies/ico/classes/http%253A%252F%252Fpurl.obolibrary.org%252Fobo%252FICO_0000065?lang=en">ICO:0000065</a>)
but I have the same reservations as for OPMI. There’s an overlap of the same
authors with OPMI, so I have reservations to invest in reusing terms they
haven’t been able to deduplicate themselves.</li>
  <li><a href="https://obofoundry.org/ontology/ero">Eagle-I Resource Ontology (ERO)</a> is an
OBO Foundry ontology that has a term
<a href="https://purl.obolibrary.org/obo/ERO_0000016">clinical trial (ERO:0000016)</a>
which nicely subclasses OBI’s investigation class, but ERO has been abandoned
and marked as deprecated in the OBO Foundry.</li>
  <li>The <a href="https://bioregistry.io/omit">Ontology for MicroRNA Target (OMIT)</a>
haphazardly imported all of MeSH at some point and has a term
<a href="https://www.ebi.ac.uk/ols4/ontologies/omit/classes/http%253A%252F%252Fpurl.obolibrary.org%252Fobo%252FOMIT_0016936">Clinical Trial (OMIT:0016936)</a>.</li>
  <li>The <a href="https://bioregistry.io/registry/cto">Clinical Trials Ontology (CTO)</a> is
an OBO Foundry ontology that nominally has the correct scope and has a term
<a href="https://purl.obolibrary.org/obo/CTO_0000220">clinical trial (CTO:0000220)</a>,
but there are potential issues with its design choices, and it was produced
by a group that has historically had difficulty maintaining its resources and
actively participating in the OBO community. Despite these issues making it
less suitable for reuse, the associated workshop proceeding
<a href="https://bioregistry.io/pmc:PMC9389640">CTO: a Community-Based Clinical Trial Ontology and its Applications in PubChemRDF and SCAIView</a>
contains some interesting ideas.</li>
</ol>

<p>Honorable mentions from non-ontology resources, which are a not ideal from a
modeling perspective to use as a parent class in an ontology:</p>

<ol>
  <li><a href="https://bioregistry.io/biolink">BioLink model</a> has a term
<a href="https://w3id.org/biolink/vocab/ClinicalTrial">clinical trial (biolink:ClinicalTrial)</a>.</li>
  <li>SNOMED has a term
<a href="http://snomed.info/id/110465008">Clinical trial (procedure) (SNOMED:110465008)</a>,
but is not an ontology and is notorious for being a closed resource,
hampering reuse.</li>
  <li>NCIT has a term for
<a href="https://bioregistry.io/NCIT:C71104">Clinical Trial (NCIT:C71104)</a>, but it is
not curated an ontology (despite OBO Foundry having an OWL conversion of it).</li>
  <li>MeSH has a term
<a href="https://bioregistry.io/mesh:D002986">Clinical Trials as Topic (mesh:D002986)</a>.</li>
</ol>

<p>Despite all of what I could find, none of these terms were part of an ontology
that I can trust. Further, most of them conflated interventional clinical
studies, i.e., clinical trials, with all other clinical studies.</p>

<p>Therefore, the next step was to get in touch with OBI and ask them to mint an
authoritative term, that also can capture the nuance in clinical studies that is
lost in the other resources. I did that in their issue tracker
<a href="https://github.com/obi-ontology/obi/issues/1831">obi-ontology/obi#1831</a>. They
were very receptive, we had a nice conversation that brought up several points,
and they challenged me to go even further than just proposing the parent terms
and begin to develop a standardized model.</p>

<h3 id="a-draft-proposal">A draft proposal</h3>

<p>The following diagram represents a draft proposal that includes several terms
that OBI could mint as well as the kinds of relations between them. This is not
a perfect proposal - its goal is to be a discussion piece for an upcoming OBI
community call. There are still several parts missing and open questions.</p>

<p><a href="https://docs.google.com/drawings/d/19BuWZ-C2iZkxDScxDy9WsAtLsItvkqT9bFtFaFkpbyA/edit?usp=sharing">
<img src="https://docs.google.com/drawings/d/e/2PACX-1vSCMXiTg3EjROweTq4RKOnqRyW-RMs7QOOiC3mhbSHa1eJtfK5ROWVRX7wh63f3m5AkAjQQ7u4VAsM-/pub?w=2628&amp;h=1629" />
</a></p>

<p>Here are some missing parts to this model that could be added incrementally:</p>

<ol>
  <li>A more detailed categorization of expanded access studies based on the
<a href="https://clinicaltrials.gov/study-basics/glossary#expanded-access-type">expanded access types</a>
and
<a href="https://clinicaltrials.gov/study-basics/glossary#expanded-access-status">expanded access status</a></li>
  <li>A more detailed categorization of observational studies based on the 1)
assembly of groups and cohorts and 2)
<a href="https://clinicaltrials.gov/study-basics/glossary#observational-study-model">observational study models</a>
such as case-control, case-only, case-cross-over, ecologic or community
studies, and family-based.</li>
  <li>A model for eligibility criteria and enrollment</li>
  <li>A model for outcomes, linked to OBI’s assay terms</li>
  <li>A model for investigators, funder types, and sponsors to support bibliometric
investigation</li>
  <li>A model for geolocation data associated with clinical study sites</li>
  <li>A model for capturing adverse events and reasons for trial cancellation/end.
In downstream applications, this could be used in tandem with the
<a href="https://www.fda.gov/drugs/surveillance/fdas-adverse-event-reporting-system-faers">FDA’s Adverse Event Reporting System (FAERS)</a>
and the
<a href="https://vaers.hhs.gov/">Vaccine Adverse Event Reporting System (VAERS)</a>.</li>
</ol>

<p>There is high potential for applying natural language processing methods to
extract more detailed information from the unstructured parts of clinical study
records. I’ve been focusing on ClincialTrials.gov as an example in this post,
but other clinical trial registries comprise almost exclusively unstructured
text.</p>

<h2 id="proof-of-concept-ontology-export-of-clinicaltrialsgov">Proof-of-concept Ontology Export of ClinicalTrials.gov</h2>

<p><a href="https://github.com/biopragmatics/pyobo">PyOBO</a> is Python software package that
implements an in-memory data structure for OBO/OWL ontologies as well as I/O
operations. On top of this, it implements workflows for converting databases
like HGNC, MeSH, and ChEMBL into OBO/OWL ontolgies. These workflows are careful
to make good design decisions, reusing classes and relations from other OBO
ontologies when possible. This is crucial for them to be readily integratable
with other resources.</p>

<p>The <a href="https://github.com/biopragmatics/obo-db-ingest"><code class="language-plaintext highlighter-rouge">obo-db-ingest</code></a> repository
is responsible for automatically downloading new versions of the resources
covered by PyOBO, converting them to OBO/OWL, archiving them to Zenodo, and
assigning persistent URLs (PURLs) so the files can be accessed in a sustainable
way. It’s also careful to include licensing information such that anyone can
download these resources in a ready-to-use format, whereas the underlying
resources are often less easy to use directly.</p>

<p>As a proof-of-concept, I implemented a converter for ClinicalTrials.gov in PyOBO
at
<a href="https://github.com/biopragmatics/pyobo/blob/main/src/pyobo/sources/clinicaltrials.py">https://github.com/biopragmatics/pyobo/blob/main/src/pyobo/sources/clinicaltrials.py</a>.
The draft converter uses temporary classes to represent the study types and
allocations I’m proposing to OBI. It also mints some of its own relationships,
which would ideally be encoded in either OBI or the Relation Ontology (RO) for
maximum reusability.</p>

<p>The initial export contains more than 500K clinical studies; nearly one million
literature references, and near two million relationships between trials,
interventions, and conditions (there are still several places for expansion and
improvement discussed below). The ClinicalTrials.gov data is licensed under an
<a href="https://clinicaltrials.gov/about-site/terms-conditions#availability">equivalent to a public domain dedication</a>,
so there are few restrictions on remixing and redistributing the data this way.</p>

<p>A summary page can be found in the <code class="language-plaintext highlighter-rouge">obo-db-ingest</code> repository
<a href="https://github.com/biopragmatics/obo-db-ingest/tree/main/export/clinicaltrials">here</a>
and the exported artifacts are listed here:</p>

<table>
  <thead>
    <tr>
      <th>Artifact</th>
      <th>Download PURL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>OBO</td>
      <td><a href="https://w3id.org/biopragmatics/resources/clinicaltrials/clinicaltrials.obo.gz">https://w3id.org/biopragmatics/resources/clinicaltrials/clinicaltrials.obo.gz</a></td>
    </tr>
    <tr>
      <td>OFN</td>
      <td><a href="https://w3id.org/biopragmatics/resources/clinicaltrials/clinicaltrials.ofn.gz">https://w3id.org/biopragmatics/resources/clinicaltrials/clinicaltrials.ofn.gz</a></td>
    </tr>
    <tr>
      <td>OWL</td>
      <td><a href="https://w3id.org/biopragmatics/resources/clinicaltrials/clinicaltrials.owl.gz">https://w3id.org/biopragmatics/resources/clinicaltrials/clinicaltrials.owl.gz</a></td>
    </tr>
    <tr>
      <td>OBO Graph JSON</td>
      <td><a href="https://w3id.org/biopragmatics/resources/clinicaltrials/clinicaltrials.json.gz">https://w3id.org/biopragmatics/resources/clinicaltrials/clinicaltrials.json.gz</a></td>
    </tr>
    <tr>
      <td>Nodes</td>
      <td><a href="https://w3id.org/biopragmatics/resources/clinicaltrials/clinicaltrials.tsv">https://w3id.org/biopragmatics/resources/clinicaltrials/clinicaltrials.tsv</a></td>
    </tr>
  </tbody>
</table>

<p>Here’s what some OBO instances for clinical studies look like for each clinical
study type:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Instance]
id: clinicaltrials:NCT00000102
name: Congenital Adrenal Hyperplasia\: Calcium Channels as Therapeutic Targets
property_value: clinicaltrials:has_intervention mesh:D009543 ! has intervention Nifedipine
property_value: clinicaltrials:investigates_condition mesh:D000308 ! investigates condition Adrenocortical Hyperfunction
property_value: clinicaltrials:investigates_condition mesh:D000312 ! investigates condition Adrenal Hyperplasia, Congenital
property_value: clinicaltrials:investigates_condition mesh:D006965 ! investigates condition Hyperplasia
property_value: clinicaltrials:investigates_condition mesh:D047808 ! investigates condition Adrenogenital Syndrome
instance_of: interventional-clinical-trial

[Instance]
id: clinicaltrials:NCT00000104
name: Does Lead Burden Alter Neuropsychological Development?
property_value: clinicaltrials:investigates_condition mesh:D007855 ! investigates condition Lead Poisoning
property_value: clinicaltrials:investigates_condition mesh:D011041 ! investigates condition Poisoning
instance_of: observational-clinical-trial

[Instance]
id: clinicaltrials:NCT00000106
name: 41.8 Degree Centigrade Whole Body Hyperthermia for the Treatment of Rheumatoid Diseases
property_value: clinicaltrials:investigates_condition mesh:D003095 ! investigates condition Collagen Diseases
property_value: clinicaltrials:investigates_condition mesh:D012216 ! investigates condition Rheumatic Diseases
instance_of: randomized-interventional-clinical-trial

[Instance]
id: clinicaltrials:NCT00000250
name: Cold Water Immersion Modulates Reinforcing Effects of Nitrous Oxide - 2
property_value: clinicaltrials:has_intervention mesh:D009609 ! has intervention Nitrous Oxide
property_value: clinicaltrials:investigates_condition mesh:D009293 ! investigates condition Opioid-Related Disorders
property_value: clinicaltrials:investigates_condition mesh:D019966 ! investigates condition Substance-Related Disorders
instance_of: non-randomized-interventional-clinical-trial

[Instance]
id: clinicaltrials:NCT00040625
name: ALIMTA \(Pemetrexed\) Alone or in Combination With Cisplatin for Patients With Malignant Mesothelioma.
property_value: clinicaltrials:has_intervention mesh:D000068437 ! has intervention Pemetrexed
property_value: clinicaltrials:investigates_condition mesh:D000086002 ! investigates condition Mesothelioma, Malignant
property_value: clinicaltrials:investigates_condition mesh:D008654 ! investigates condition Mesothelioma
instance_of: expanded-access-study
</code></pre></div></div>

<h2 id="reflections-and-whats-missing">Reflections, and, what’s missing?</h2>

<p>I first became familiar with ClinicalTrials.gov and other clinical study
registries while working on the
<a href="https://globalbiodefense.com/2023/05/10/rapter-expediting-medical-countermeasure-response/">RAPTER project</a>,
funded by the America Defense Threat Reduction Agency (DTRA) with the goal to
integrate vaccine information and build computational tools to quicken the
development of vaccines in response to future pandemics.</p>

<p>One of the key issues to overcome was the accuracy of the data within these
resources. For example, ClinicalTrials.gov contains both a free-text and
processed field for its conditions and interventions. In many cases,
re-processing was required to ensure complete and accurate information. In my
draft export of ClincalTrials.gov, I exclusively used the processed data fields,
but a more careful conversion would require additional data science techniques.</p>

<p>Similarly, for data from the World Health Organization and other clinical trial
registries, full NER and relation extraction is required to identify
interventions, conditions, outcomes, and other fields.</p>

<p>Even when processed data is available, it’s using MeSH identifiers, which are
not readily integrable with other resources. That’s why ontologies often curate
MeSH cross-references themselves. I created the
<a href="https://github.com/biopragmatics/biomappings">Biomappings</a> project as a way to
quickly predict and curate MeSH mappings to other chemical and disease
vocabularies, such as
<a href="https://bioregistry.io/chebi">Chemical Entities of Biological Interest (ChEBI)</a>
and the <a href="https://bioregistry.io/doid">Disease Ontology (DOID)</a>. I also built
<a href="https://github.com/biopragmatics/semra">SeMRA</a>, a workflow for assembling and
inferring mappings to best reuse existing mappings available from a wide variety
of sources. These two approaches are crucial for making clinical study and trial
information actionable in a data integration scenario.</p>

<p>I didn’t even mention the PICO (patient/population, intervention, comparison and
outcomes) data model - this is a whole other can of worms for a different
discussion. I will try to come back to that if I get the chance to write up some
of my work with Jeremy Zucker’s team at PNNL on the
<a href="https://github.com/y0-causal-inference/y0">y0 Causal Reasoning Engine</a> and
Judea Pearl-style causal inference applications in clinical study statistical
analysis.</p>

<hr />

<p>There’s a lot of work to do in this space, but this is a nice first step. For
me, exporting lots of resources in a standard ontology format makes it easy to
load up a complete set of nodes when building knowledge graphs. As a coda, I
will say a little bit about what I like to do with this kind of data once I have
got it structured and integrated with other sources.</p>

<h2 id="whats-this-all-useful-for-anyway">What’s this all useful for, anyway?</h2>

<p>My work in the past decade has focused on constructing and applying knowledge
graphs to tasks in drug discovery. While a large part of this has incorporated
machine learning, artificial intelligence, and causal inference, many problems
can be formulated as queries over a graph-like data structure.</p>

<p>In this last section, I’m going to give a few examples of graph queries that can
enable experts to explore and generate interesting, explainable, testable
hypotheses. In practice, I have been using Neo4j and RDF as graph models with
their respective Cypher and SPARQL query languages, but the case studies here
will stay at a high level.</p>

<h3 id="maximum-phase-for-a-drug">Maximum Phase for a Drug</h3>

<pre><code class="language-mermaid">   graph LR
     drug[Drug] -- intervention in --&gt; trial[Clinical Trial] -- has phase --&gt; phase[Phase]
     drug -. has maximum phase (aggregated) .-&gt; phase
</code></pre>

<p>Because the same intervention may appear in multiple clinical trials, it’s
useful to know the maximum phase reached over all trials. If the maximum phase
is four, then it can be concluded that the drug was approved for use. If the
maximum phase is three (and they are all completed), then it can be concluded
that the drug ultimately failed to be effective. Such drugs are can be good
candidates for repositioning since the expensive safety studies have already
been run.</p>

<h3 id="maximum-phase-for-a-scaffold-or-substructure">Maximum Phase for a Scaffold or Substructure</h3>

<pre><code class="language-mermaid">   graph LR
     substructure[Substructure] -- substructure of --&gt; drug[Drug] -- intervention in --&gt; trial[Clinical Trial] -- has phase --&gt; phase
     substructure -. has maximum phase (aggregated) .-&gt; phase
</code></pre>

<p>For small molecule drugs, the principle of structure-activity relationship (SAR)
states that there is often a high correlation between the chemical structure and
its functional activity. Many similar drugs work because they share a privileged
substructure. For example, the beta-hydroxy lactone appearing in statins enable
their HMC-CoA reducatase inhibitor activity, which confers their ability to
reduce LDL and risk of cardiovascular disease. Similarly, sulfonamides were a
classic substructure used in first-generation antibacterials due to their
competitive inhibition of dihydropteroate synthase, a key part of folate
biosynthesis in most non-human, non-eukaryote organisms.</p>

<p>Ontologies like ChEBI contain relationships between these substructures (and
scaffolds) and chemicals. By combining ChEBI and ClinicalTrials.gov in a single
graph, the previous query that aggregated maximum phase over a specific chemical
can be generalized to substructures. Further, the “substructure of” relationship
is transitive, meaning that multiple substructure relations can be chained
together to ask the same question at many levels of granularity.</p>

<p>Ultimately, you can get results like <em>beta-hydroxy lactone have appeared in
clinical trials of a maximum phase of 4</em> and ask even more granular questions,
like what is the maximum clinical phase for drugs with an arsenic atom as part
of their structure (surprisingly, the answer is more than 0!). On the flip side,
this can either identify opportunities for structures that haven’t been in
clinical trials, or beg the question of why they haven’t so far.</p>

<p>Note that the aggregation over maximum phase is just an example - being able to
query over a combination of one resource (here, ClinicalTrials.gov) and the
hierarchy of another (here, ChEBI) is a more general and powerful concept.</p>

<h3 id="maximum-phase-for-pairs-of-substructures-and-diseases">Maximum Phase for Pairs of Substructures and Diseases</h3>

<pre><code class="language-mermaid">   graph LR
     substructure[Substructure] -- substructure of --&gt; drug[Drug] -- intervention in --&gt; trial[Clinical Trial] -- studies --&gt; disease[Disease]
     trial -- has phase --&gt; phase[Phase]
     substructure -. has maximum phase (aggregated) .-&gt; phase
</code></pre>

<p>The previous query becomes even more powerful when extending the path from the
clinical trial to a disease. Then, the aggregation operation can be done over
the pair of the substructure and the disease to answer questions like <em>what’s
the maximum phase that arsenic-containing drugs have been used in for each
disease?</em>, or, more specifically, <em>what’s the maximum phase that
arsenic-containing drugs have been used in for malaria?</em></p>

<h3 id="maximum-phase-for-pairs-of-substructures-and-disease-classes">Maximum Phase for Pairs of Substructures and Disease Classes</h3>

<pre><code class="language-mermaid">   graph LR
     substructure[Substructure] -- substructure of --&gt; drug[Drug] -- intervention in --&gt; trial[Clinical Trial] -- studies --&gt; disease[Disease] -- subclass of --&gt; diseaseclass[Disease Class]
     trial -- has maximum phase --&gt; phase[Phase]
     substructure -. has maximum phase .-&gt; phase
</code></pre>

<p>The second case study extended the way we look at drugs by adding the
hieararchical substructure relationships from ChEBI. If we want to aggregate
diseases at a different granularity, we can do so by incorporating the subclass
relationships from a resource like the Disease Ontology. Then, we can ask the
previous question at a chosen level of granularity, like <em>what’s the maximum
phase that arsenic-containing drugs have been used in for each parasitic
infectious disease?</em></p>

<p>It’s a bit tricky to ask the open-ended question <em>what’s the maximum phase that
arsenic-containing drugs have been used in for each disease class?</em> because it
depends on the construction of the disease hierarchy. Other common aggregations
here are for rare diseases, cancers, neurodegenerative diseases, etc. In
practice, I usualy have an additional way of tagging the terms in the hierarchy
that I want to aggregate on, either by labeling the node in a property graph, or
using yet another relationship to a node representing a grouping of my desired
query terms.</p>

<h3 id="maximum-phase-for-pairs-of-vaccine-platforms-and-disease-classes">Maximum Phase for Pairs of Vaccine Platforms and Disease Classes</h3>

<pre><code class="language-mermaid">   graph LR
     vaccineplatform[Vaccine Platform] -- platform for --&gt; vaccine[Vaccine] -- intervention in --&gt; trial[Clinical Trial] -- studies --&gt; disease[Disease] -- subclass of --&gt; diseaseclass[Disease Class]
     trial -- has phase --&gt; phase[Phase]
     vaccineplatform -. has maximum phase .-&gt; phase
</code></pre>

<p>The original use case for a query like the one before was for the RAPTER
project, where we were asked to summarize for each vaccine platform (e.g., RNA
vaccines, DNA vaccines, viral vector vaccines) what was the maximum clinical
trial phase for trials over COVID-19 and a few other classes of parasitic and
bacterial infections. The diagram above has the same shape and flavor as the
previous one with substructures, chemicals, and diseases except the type of
relation and direction between a vaccine and a vaccine platform is different.
Despite this, the query is effectively shaped the same.</p>

<h3 id="phenotypic-drug-discovery-scenarios">Phenotypic Drug Discovery Scenarios</h3>

<p>When there exists a good cellular model of disease or good model organisms, it’s
possible to eschew the typical target focus of a drug discovery campaign.
However, during or after a phenotypic drug discovery, it’s useful to uncover a
deeper mechanistic context through a combination of target identification and
mechanism of action deconvolution.</p>

<h4 id="target-identification">Target Identification</h4>

<pre><code class="language-mermaid">graph LR
  disease[Disease] -- studied in --&gt; trial[Clinical Trial] -- uses intervention --&gt; drug[Drug] -- regulates --&gt; protein[Protein]
  disease -. has putative target .-&gt; protein
</code></pre>

<p>Target identification is typically the process of identifying a protein whose
modulation in a given disease context can result in a therapeutic effect.</p>

<p>If we have a successful clinical trial performed following a phenotypic drug
discovery campaign, it might not be known what targets it modulates. Therefore,
we can combine the clinical trial data with a chemical activity database like
ChEMBL, so we can get all the targets for the chemical, then hypothesize that
one or more of them are responsible for the drug’s therapeutic effect.</p>

<p>This is even more powerful when combining other databases like OpenTargets,
which aggregates a wide variety of orthogonal evidence and provide workflows and
resources for further triage and DisGeNet, which aggregates text mining
co-occurrence evidence as a proxy for association, which can be useful when done
at scale with the appropriate statistical adjustments.</p>

<p>Finally, target identification hypotheses are testable, especially following a
phenotypic drug discovery campaign with a good cellular model or model organism.
Tool compounds that are specific modulators for the target or genomic
experiments like knockdowns, knockouts, or over-expression can provide more
confident confirmation of the target’s viability.</p>

<h4 id="mechanism-of-action-deconvolution">Mechanism of Action Deconvolution</h4>

<pre><code class="language-mermaid">graph LR
  drug[Drug] -- intervention in --&gt; trial[Clinical Trial] -- studies --&gt; disease[Disease] -- has target --&gt; protein[Protein]
  drug -. has putative mechanism of action .-&gt; protein
</code></pre>

<p>The dual problem to target identification is mechanism of action (MoA)
deconvolution. After a successful clinical trial following a phenotypic drug
discovery campaign, but not know the target that your drug modulates. You can
get the target information by integrating parts of a database like OpenTargets
or DisGeNet, then you will be able to use the path query to propose putative
targets.</p>

<p>MoA hypotheses are often easier to test than target identification hypotheses
because they can be done in biochemical assays. However, there are lots of
tricky targets (including ones that aren’t proteins) where this isn’t so
straightforward.</p>

<p>For both target identification and MoA, you might start imagining ways to take
into account chemical similarity, protein similarity, and other hierarchical
structures for more sophisticated queries. Thinking about this is why I’ve loved
transitioning from a medicinal chemist in my early career to a
chem/bioinformatician in my mid-career. I’ll leave the last part as an exercise
to the reader, or for someone who wants to pay for consultation ;)</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="clinical trials" /><category term="ClinicalTrials.gov" /><category term="ontologies" /><category term="OBI" /><category term="ChEBI" /><category term="Disease Ontology" /><category term="reproducibility" /><category term="data integration" /><category term="knowledge graphs" /><category term="graph database" /><category term="graph query" /><summary type="html"><![CDATA[I’ve recently worked with clinical studies from ClinicalTrials.gov and other international registries. This post is a review on how to access data, a proposal for how it can be modeled using the Ontology for Biomedical Investigations (OBI), a proof-of-concept ontologization of ClinicalTrials.gov, and some insights into how this data can be integrated with other resources to address classical problems in drug discovery from a knowledge graph perspective.]]></summary></entry><entry><title type="html">Books I Read in 2024</title><link href="https://cthoyt.com/2025/01/18/books-in-2024.html" rel="alternate" type="text/html" title="Books I Read in 2024" /><published>2025-01-18T08:33:00+00:00</published><updated>2025-01-18T08:33:00+00:00</updated><id>https://cthoyt.com/2025/01/18/books-in-2024</id><content type="html" xml:base="https://cthoyt.com/2025/01/18/books-in-2024.html"><![CDATA[<p>Here’s the books I read in 2024. If I were Dudley Dursley, I’d be very upset
that I read one fewer new book than <a href="/2024/01/01/books-in-2023.html">in
2023</a>. But then, I’d remember that I
re-read a lot of Cosmere in 2024 to prepare for <em>Wind and Truth</em>, which was
great.</p>

<ol>
  <li>Iron Gold (Red Rising #4) by Pierce Brown</li>
  <li>Dark Age (Red Rising #5) by Pierce Brown</li>
  <li>Light Bringer (Red Rising #6) by Pierce Brown</li>
  <li>The Fifth Season (Broken Earth #1) by N.K. Jemisin</li>
  <li>Emma by Jane Austen</li>
  <li>The Obelisk Gate (Broken Earth #2) by N.K. Jemisin</li>
  <li>The Slow Regard of Silent Things (Kingkiller Chronicles) by Patrick Rothfuss</li>
  <li>The Narrow Road Between Desires (Kingkiller Chronicles) by Patrick Rothfuss</li>
  <li>The Three Body Problem (Remembrance of Earth’s Past #1) by Cixin Liu</li>
  <li>The Dark Forest (Remembrance of Earth’s Past #2) by Cixin Liu</li>
  <li>Death’s End (Remembrance of Earth’s Past #3) by Cixin Liu</li>
  <li>Artificial Condition (Murderbot Diaries #2) by Martha Wells</li>
  <li>Rogue Protocol (Murderbot Diaries #3) by Martha Wells</li>
  <li>Exit Strategy (Murderbot Diaries #4) by Martha Wells</li>
  <li>A Deadly Education (Scholomance #1) by Naomi Novik</li>
  <li>Network Effect (Murderbot Diaries #5) by Martha Wells</li>
  <li>Fugitive Telemetry (Murderbot Diaries #6) by Martha Wells</li>
  <li>System Collapse (Murderbot Diaries #7) by Martha Wells</li>
  <li>The Last Graduate (Scholomance #2) by Naomi Novik</li>
  <li>The Golden Enclaves (Scholomance #3) by Naomi Novik</li>
  <li>The Machine Stops by E.M. Forster</li>
  <li>Grief is the Thing with Feathers by Max Porter</li>
  <li>A Natural History of Dragons (Lady Trent #1) by Marie Brennan</li>
  <li>The Tropic of Serpents (Lady Trent #2) by Marie Brennan</li>
  <li>A Journey to the Center of the Earth by Jules Verne</li>
  <li>Simple Passion by Annie Ernaux</li>
  <li>Exhalation by Ted Chiang</li>
  <li>Before the Coffee Gets Cold by Toshikazu Kawaguchi</li>
  <li>The Sirens of Titan by Kurt Vonnegut Jr.</li>
  <li>What You Can See from Here by Mariana Leky</li>
  <li>The Rithmatist by Brandon Sanderson</li>
  <li>The Voyage of the Basilisk (Lady Trent #3) by Marie Brennan</li>
  <li>A Court of Thorns and Roses (ACOTAR #1) by Sarah J. Maas</li>
  <li>The Stone Sky (Broken Earth #3) by N.K. Jemisin</li>
  <li>The Alchemist by Paulo Coelho</li>
  <li>In the Labyrinth of Drakes (Lady Trent #4) by Marie Brennan</li>
  <li>Within the Sanctuary of Wings (Lady Trent #5) by Marie Brennan</li>
  <li>Indigo Ridge by Perry Devney</li>
  <li>City of Stairs (Divine Cities #1) by Robert Jackson Bennett</li>
  <li>Bartleby the Scrivner by Herman Melville</li>
  <li>Beowulf</li>
  <li>Wind and Truth (Stormlight Archives #5) by Brandon Sanderson</li>
</ol>

<h3 id="rereads">Rereads</h3>

<ul>
  <li>Cosmere: Stormlight Archive 1-4 and Warbreaker</li>
  <li>Kingkiller Chronicles: Name of the Wind and A Wise Man’s Fear</li>
</ul>

<h3 id="what-i-loved-minor-spoilers">What I Loved (minor spoilers):</h3>

<ol>
  <li>Every single short story in Exhalation. You can check out my favorite,
<a href="https://images.shulcloud.com/1202/uploads/Documents/TheMerchantandtheAlchemistsGate.pdf">The Merchant and the Alchemist’s Gate</a>,
which is about how to find meaning in time travel, even if you can’t change
anything.</li>
  <li>The Alchemist. It beats you over the head with its moral philosophy about
seizing your destiny. It’s great.</li>
  <li>The sci-fi in the whole Three Body Problem trilogy. Lots of great ideas, even
if the characters were weak at moments. I initially didn’t like Luo Ji, but
he eventually had a great arc which I’ve come to understand follows a more
typical Chinese archetype.</li>
  <li>Lady Trent has such a great tone and voice as a narrator. The epistolary
format worked so well for these books.</li>
  <li>Fifth ideals!</li>
</ol>

<h3 id="non-superlative-comments">Non-superlative Comments</h3>

<ol>
  <li>Last year, I wrote that I wasn’t a big fan of Murderbot #1. I stand by my
criticism of the first book, but I’m glad I tried the rest, because it
addressed my qualms and was a lot of fun.</li>
  <li>ACOTAR would have definitely benefited from having a major revision to
tighten up the story and characters, but I am sad when people trash on it. I
enjoyed it.</li>
</ol>

<h3 id="disappointments">Disappointments</h3>

<ol>
  <li>I started Persuasion by Jane Austen, but I didn’t care for any of the
characters, so I haven’t finished it yet.</li>
  <li>I heard so many times that The Fifth Season and its sequels were great. The
first book had a cool premise and cool reveal at the end, but the second and
third book just didn’t have any good characters or plot to hold it together.
I should have put it down after the second book, but I just assumed that
since each installment won a Hugo award, that there was a massive payoff at
the end. There was not.</li>
  <li>There weren’t enough Brandon Sandwiches, so I read his non-Cosmere novel, The
Rithmatist, that will never get a sequel. When will I learn?</li>
  <li>I’m very disappointed by the Netflix adaptation of Three Body Problem for not
doing a better job of rewriting the characters. How can anyone in Hollywood
trust Benioff and Weiss after Game of Thrones?</li>
</ol>

<h3 id="funny-story">Funny Story</h3>

<p>When I was in Gaspé over the summer, we were hanging out at a cafe that
suggested Simple Passion by Annie Ernaux as the book of the week. My sister and
I figured, we could read it and make some friends that week if they also had. We
didn’t find anyone else who even noticed the sign about it being book of the
week.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="books" /><summary type="html"><![CDATA[Here’s the books I read in 2024. If I were Dudley Dursley, I’d be very upset that I read one fewer new book than in 2023. But then, I’d remember that I re-read a lot of Cosmere in 2024 to prepare for Wind and Truth, which was great.]]></summary></entry><entry><title type="html">Exploring Event Venues in Wikidata</title><link href="https://cthoyt.com/2025/01/17/event-venues-in-wikidata.html" rel="alternate" type="text/html" title="Exploring Event Venues in Wikidata" /><published>2025-01-17T17:08:00+00:00</published><updated>2025-01-17T17:08:00+00:00</updated><id>https://cthoyt.com/2025/01/17/event-venues-in-wikidata</id><content type="html" xml:base="https://cthoyt.com/2025/01/17/event-venues-in-wikidata.html"><![CDATA[<p>I was working on making data about scholarly conferences more FAIR and a big
question crossed my mind: what are all the conference venues? This post is about
some queries I wrote for Wikidata, data issues I found, and a few
<a href="https://www.qeios.com/read/KBX9VO">drive-by curations</a> that I did while looking
for an answer, and my ideas for the future.</p>

<h2 id="querying-wikidata">Querying Wikidata</h2>

<p>Wikidata is always a good place to start looking for structured data because it
contains a detailed and multidisciplinary ontology whose classes, subclass
relationships, and instances are all queryable via SPARQL.</p>

<p>It has a top-level class for
<a href="https://www.wikidata.org/wiki/Q18674739">event venues (Q18674739)</a>, that even
comes with a high-level schematic diagram on how this class relates to
performance arts spaces:</p>

<p><img src="https://upload.wikimedia.org/wikipedia/commons/6/61/Core_classes_for_performing_arts_places_in_Wikidata.png" alt="" /></p>

<p>I used the following SPARQL query to retrieve all the subclasses and investigate
which might be relevant for conferences.</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="nv">?venueType</span><span class="w"> </span><span class="nv">?venueTypeLabel</span><span class="w"> </span><span class="nv">?venueTypeDescription</span><span class="w">
</span><span class="k">WHERE</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nv">?venueType</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P279</span><span class="o">*</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q18674739</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">bd</span><span class="o">:</span><span class="ss">serviceParam</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">language</span><span class="w"> </span><span class="s2">"[AUTO_LANGUAGE],mul,en"</span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The results from this query are embedded in the table below:</p>

<iframe style="width: 80vw; height: 50vh; border: none;" src="https://query.wikidata.org/embed.html#SELECT%20DISTINCT%20%3FvenueType%20%3FvenueTypeLabel%20%3FvenueTypeDescription%0AWHERE%0A%7B%0A%20%20%3FvenueType%20wdt%3AP279%2a%20wd%3AQ18674739%20.%0A%20%20SERVICE%20wikibase%3Alabel%20%7B%20bd%3AserviceParam%20wikibase%3Alanguage%20%22%5BAUTO_LANGUAGE%5D%2Cmul%2Cen%22.%20%7D%0A%7D" referrerpolicy="origin" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>

<p>The results contain a lot of irrelevant venue types such as cinemas, theaters,
and clubs. It’s interesting for me to make a list of these, and refine the query
to exclude them like so:</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="nv">?venueType</span><span class="w"> </span><span class="nv">?venueTypeLabel</span><span class="w"> </span><span class="nv">?venueTypeDescription</span><span class="w">
</span><span class="k">WHERE</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nv">?venueType</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P279</span><span class="o">*</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q18674739</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">FILTER</span><span class="p">(</span><span class="nv">?venueType</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q41253</span><span class="p">,</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q622425</span><span class="p">))</span><span class="w">  </span><span class="c1"># this list is shortened for demo purposes</span><span class="w">
  </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">bd</span><span class="o">:</span><span class="ss">serviceParam</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">language</span><span class="w"> </span><span class="s2">"[AUTO_LANGUAGE],mul,en"</span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="data-exploration">Data Exploration</h2>

<p>I encountered two noteworthy data issues the first time I ran this query:</p>

<ol>
  <li>Some results had Wikidata identifiers in the labels column</li>
  <li>Some results appeared to be for specific event venues, and not classes</li>
</ol>

<p>The first issue can be attributed to the fact that not all Wikidata records have
labels in english. This doesn’t mean that they aren’t valuable - it’s also an
opportunity for doing manual or semi-automated translation to improve the data
in Wikidata.</p>

<p>The second issue is related to the ontology structure of Wikidata. Because this
is a query over subclasses of event venues, specific venues shouldn’t show up.
This is because Wikidata records representing specific venues should use the
<a href="https://www.wikidata.org/wiki/Property:P31">instance of (P31)</a> relationship to
annotate what kind/type of event venue they are.</p>

<p>I identified a couple groups of event venues that seemed to have this issue:</p>

<ul>
  <li>dive sites, like <a href="https://www.wikidata.org/wiki/Q14213916">Q14213916</a></li>
  <li>cinemas in Brazil, like <a href="https://www.wikidata.org/wiki/Q123856958">Q123856958</a></li>
  <li>museums in Sweden, like <a href="https://www.wikidata.org/wiki/Q60628067">Q60628067</a></li>
</ul>

<p>I was able to go through and update the relationship types to be correct, so if
you’re reading this later, you won’t see the issue. That’s the power of
<a href="https://www.qeios.com/read/KBX9VO">drive-by curation</a>!</p>

<h2 id="follow-up">Follow-up</h2>

<p>Next steps are to make a spreadsheet where I curate for each event venue
subclass if they’re relevant or not.</p>

<p>After that, then I can construct a SPARQL query that uses the <code class="language-plaintext highlighter-rouge">VALUES</code> syntax to
pick the list of superclasses, then the <code class="language-plaintext highlighter-rouge">wdt:P31/wdt:P279*</code> relation path to get
all instances of any subclass of the selected event venue classes.</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="nv">?venue</span><span class="w"> </span><span class="nv">?venueLabel</span><span class="w">
</span><span class="k">WHERE</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="k">VALUES</span><span class="w"> </span><span class="nv">?venueType</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q1329623</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="nv">?venue</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P31</span><span class="o">/</span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P279</span><span class="o">*</span><span class="w"> </span><span class="nv">?venueType</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">bd</span><span class="o">:</span><span class="ss">serviceParam</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">language</span><span class="w"> </span><span class="s2">"[AUTO_LANGUAGE],mul,en"</span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>At the time of writing, this query resulted in 8,728 distinct records in about
27 seconds.</p>

<hr />

<p>While Wikidata is a great place to start, its nature as a fully open and
community-driven resource has the drawback of being less trustworthy than
resources that have an expert curation component (whether manual or
semi-automated).</p>

<p>Persistent Identifier (PID) resources like the
<a href="https://ror.org/">Research Organization Registry (ROR)</a> demonstrated that
there’s added value to having an expert-curated layer that both draws from and
contributes back to Wikidata.</p>

<p>I’m working towards seeding a registry for event venues with a similar
philosophy at
<a href="https://github.com/event-venue-registry/evr">https://github.com/event-venue-registry/evr</a>.
Importantly, this resource will follow the
<a href="https://www.nature.com/articles/s41597-024-03406-w">Open Data, Open Code, Open Infrastructure (O3) principles</a>,
meaning that the data and code are all in one GitHub repository that anyone can
contribute to. Please reach out if you’d like to help.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="Wikidata" /><category term="bibliometrics" /><category term="open data" /><summary type="html"><![CDATA[I was working on making data about scholarly conferences more FAIR and a big question crossed my mind: what are all the conference venues? This post is about some queries I wrote for Wikidata, data issues I found, and a few drive-by curations that I did while looking for an answer, and my ideas for the future.]]></summary></entry><entry><title type="html">Notes on Open Source Funding</title><link href="https://cthoyt.com/2024/12/03/open-source-funding.html" rel="alternate" type="text/html" title="Notes on Open Source Funding" /><published>2024-12-03T17:08:00+00:00</published><updated>2024-12-03T17:08:00+00:00</updated><id>https://cthoyt.com/2024/12/03/open-source-funding</id><content type="html" xml:base="https://cthoyt.com/2024/12/03/open-source-funding.html"><![CDATA[<p>This stub post contains my notes about funding for open source software. It
doesn’t follow a story like a lot of my posts, and is more like an ever-evolving
notes sheet.</p>

<p>Here’s a list of resources for getting funding for open source, which also
covers fiscal sponsorship:</p>

<ul>
  <li><a href="https://floss.fund/">FLOSS/fund</a></li>
  <li><a href="https://opencollective.com/">Open Collective</a></li>
  <li><a href="https://numfocus.org/">NumFOCUS</a></li>
  <li><a href="https://chanzuckerberg.com/eoss/">CZI EOSS</a></li>
</ul>

<p>Interesting blog posts about open source funding:</p>

<ul>
  <li>https://kvinogradov.com/algo-sponsors/</li>
  <li>https://opensourcepledge.com/</li>
  <li>https://heidiseibold.kit.com/posts/how-to-get-funding-by-caring-about-research-software-i
(this one is really good, basically a superset of my list)</li>
</ul>

<hr />

<p>Please feel free to suggest things in the comments.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="funding" /><category term="open source" /><summary type="html"><![CDATA[This stub post contains my notes about funding for open source software. It doesn’t follow a story like a lot of my posts, and is more like an ever-evolving notes sheet.]]></summary></entry><entry><title type="html">Downloading Audio from Soundcloud</title><link href="https://cthoyt.com/2024/12/03/opus-to-m4b.html" rel="alternate" type="text/html" title="Downloading Audio from Soundcloud" /><published>2024-12-03T09:11:00+00:00</published><updated>2024-12-03T09:11:00+00:00</updated><id>https://cthoyt.com/2024/12/03/opus-to-m4b</id><content type="html" xml:base="https://cthoyt.com/2024/12/03/opus-to-m4b.html"><![CDATA[<p>Brandon Sanderson has been releasing a few chapters a week of his upcoming
novel, <a href="https://bioregistry.io/isbn:1250319188">Wind and Truth</a>, on his
publisher’s website leading up to its December 6<sup>th</sup> release. This
includes the audiobook chapters, but they’re posted to Soundcloud and there’s no
good way to listen at 1.6x speed. This post is a note sheet on how to download
audio from Soundcloud and prepare it for my audiobook reader.</p>

<h2 id="get-a-package-manager">Get a package manager</h2>

<p>First step is to get <a href="https://github.com/astral-sh/uv">uv</a>. I’ve been very
excited to watch this fast tool replace many iffy Python packaging tools!</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-LsSf</span> https://astral.sh/uv/install.sh | sh
</code></pre></div></div>

<p>See also the full
<a href="https://docs.astral.sh/uv/getting-started/installation/">installation documentation</a>.</p>

<h2 id="install-the-downloader-tool">Install the downloader tool</h2>

<p><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a> is <em>a feature-rich command-line
audio/video downloader with support for
<a href="https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md">thousands of sites</a></em>.</p>

<p>Second step is to install <code class="language-plaintext highlighter-rouge">yt-dlp</code> using <code class="language-plaintext highlighter-rouge">uv tool install</code>. uv installs the tool
and its CLI in an isolated virtual environment but makes it available globally
on your system.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>uv tool <span class="nb">install </span>yt-dlp
<span class="go">Resolved 1 package in 106ms
Installed 1 package in 28ms
 + yt-dlp==2024.11.18
Installed 1 executable: yt-dlp
</span></code></pre></div></div>

<p>Look how fast it installs with a warm cache!</p>

<h2 id="download-the-audio">Download the audio</h2>

<p>The chapters for <em>Wind and Truth</em> are indexed
<a href="https://reactormag.com/columns/wind-and-truth/">here</a>. For example, you can
navigate to
<a href="https://reactormag.com/read-wind-and-truth-by-brandon-sanderson-interludes-3-and-4/">interludes 3 and 4</a>,
scroll to the bottom, then open the Soundcloud embed in a new browser tab and
copy the URL.</p>

<p>Third step is to invoke <code class="language-plaintext highlighter-rouge">yt-dlp</code> from the console with the URL as the only
argument (in quotes!). The best thing about this tool is that it has a
plugin-based architecture, so it can deal with basically anything you give it.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>yt-dlp <span class="s2">"https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/1931715512%3Fsecret_token%3Ds-WvtAtAMqqkc&amp;color=%23ff5500&amp;auto_play=false&amp;hide_related=false&amp;show_comments=true&amp;show_user=true&amp;show_reposts=false&amp;show_teaser=true&amp;visual=true"</span>
<span class="go">[SoundcloudEmbed] Extracting URL: https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/1931715512%3Fsecret_toke...ser=true&amp;visual=true
[soundcloud] Extracting URL: https://api.soundcloud.com/tracks/1931715512?secret_token=s-WvtAtAMqqkc
[soundcloud] 1931715512: Downloading info JSON
[soundcloud] 1931715512: Downloading hls_mp3 format info JSON
[soundcloud] 1931715512: Downloading http_mp3 format info JSON
[soundcloud] 1931715512: Downloading hls_opus format info JSON
[info] 1931715512: Downloading 1 format(s): hls_opus_64
[hlsnative] Downloading m3u8 manifest
[hlsnative] Total fragments: 136
[download] Destination: Interludes - WIND AND TRUTH by Brandon Sanderson, narrated by Kate Reading and Michael Kramer [1931715512].opus
[download] 100% of   10.10MiB in 00:00:04 at 2.14MiB/s
</span></code></pre></div></div>

<h2 id="convert-the-audios-format">Convert the audio’s format</h2>

<p>The only issue is that it gives output in the
<a href="https://en.wikipedia.org/wiki/Opus_(audio_format)">Opus</a> file format, which
isn’t so easy to use. The fourth step is to convert this to
<a href="https://en.wikipedia.org/wiki/MP4_file_format#Filename_extensions">m4b</a> for
easy use with an audiobook reader. After installing ffmpeg (e.g., with
<code class="language-plaintext highlighter-rouge">brew install ffmpeg</code>), run</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ffmpeg <span class="se">\</span>
    <span class="nt">-i</span> <span class="s2">"Interludes - WIND AND TRUTH by Brandon Sanderson, narrated by Kate Reading and Michael Kramer [1931715512].opus"</span> <span class="se">\</span>
    <span class="nt">-c</span>:a aac <span class="nt">-b</span>:a 128k <span class="nt">-vn</span> <span class="nt">-f</span> ipod interludes-3-and-4.m4b
ffmpeg version 7.1 Copyright <span class="o">(</span>c<span class="o">)</span> 2000-2024 the FFmpeg developers
  built with Apple clang version 16.0.0 <span class="o">(</span>clang-1600.0.26.4<span class="o">)</span>
  configuration: <span class="nt">--prefix</span><span class="o">=</span>/opt/homebrew/Cellar/ffmpeg/7.1_3 <span class="nt">--enable-shared</span> <span class="nt">--enable-pthreads</span> <span class="nt">--enable-version3</span> <span class="nt">--cc</span><span class="o">=</span>clang <span class="nt">--host-cflags</span><span class="o">=</span> <span class="nt">--host-ldflags</span><span class="o">=</span><span class="s1">'-Wl,-ld_classic'</span> <span class="nt">--enable-ffplay</span> <span class="nt">--enable-gnutls</span> <span class="nt">--enable-gpl</span> <span class="nt">--enable-libaom</span> <span class="nt">--enable-libaribb24</span> <span class="nt">--enable-libbluray</span> <span class="nt">--enable-libdav1d</span> <span class="nt">--enable-libharfbuzz</span> <span class="nt">--enable-libjxl</span> <span class="nt">--enable-libmp3lame</span> <span class="nt">--enable-libopus</span> <span class="nt">--enable-librav1e</span> <span class="nt">--enable-librist</span> <span class="nt">--enable-librubberband</span> <span class="nt">--enable-libsnappy</span> <span class="nt">--enable-libsrt</span> <span class="nt">--enable-libssh</span> <span class="nt">--enable-libsvtav1</span> <span class="nt">--enable-libtesseract</span> <span class="nt">--enable-libtheora</span> <span class="nt">--enable-libvidstab</span> <span class="nt">--enable-libvmaf</span> <span class="nt">--enable-libvorbis</span> <span class="nt">--enable-libvpx</span> <span class="nt">--enable-libwebp</span> <span class="nt">--enable-libx264</span> <span class="nt">--enable-libx265</span> <span class="nt">--enable-libxml2</span> <span class="nt">--enable-libxvid</span> <span class="nt">--enable-lzma</span> <span class="nt">--enable-libfontconfig</span> <span class="nt">--enable-libfreetype</span> <span class="nt">--enable-frei0r</span> <span class="nt">--enable-libass</span> <span class="nt">--enable-libopencore-amrnb</span> <span class="nt">--enable-libopencore-amrwb</span> <span class="nt">--enable-libopenjpeg</span> <span class="nt">--enable-libspeex</span> <span class="nt">--enable-libsoxr</span> <span class="nt">--enable-libzmq</span> <span class="nt">--enable-libzimg</span> <span class="nt">--disable-libjack</span> <span class="nt">--disable-indev</span><span class="o">=</span>jack <span class="nt">--enable-videotoolbox</span> <span class="nt">--enable-audiotoolbox</span> <span class="nt">--enable-neon</span>
  libavutil      59. 39.100 / 59. 39.100
  libavcodec     61. 19.100 / 61. 19.100
  libavformat    61.  7.100 / 61.  7.100
  libavdevice    61.  3.100 / 61.  3.100
  libavfilter    10.  4.100 / 10.  4.100
  libswscale      8.  3.100 /  8.  3.100
  libswresample   5.  3.100 /  5.  3.100
  libpostproc    58.  3.100 / 58.  3.100
Input <span class="c">#0, ogg, from 'Interludes - WIND AND TRUTH by Brandon Sanderson, narrated by Kate Reading and Michael Kramer [1931715512].opus':</span>
  Duration: 00:22:12.36, start: 0.000000, bitrate: 63 kb/s
  Stream <span class="c">#0:0: Audio: opus, 48000 Hz, stereo, fltp</span>
      Metadata:
        encoder         : Lavc58.91.100 libopus
Stream mapping:
  Stream <span class="c">#0:0 -&gt; #0:0 (opus (native) -&gt; aac (native))</span>
Press <span class="o">[</span>q] to stop, <span class="o">[</span>?] <span class="k">for </span><span class="nb">help
</span>Output <span class="c">#0, ipod, to 'interludes-3-and-4.m4b':</span>
  Metadata:
    encoder         : Lavf61.7.100
  Stream <span class="c">#0:0: Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 128 kb/s</span>
      Metadata:
        encoder         : Lavc61.19.100 aac
<span class="o">[</span>out#0/ipod @ 0x6000016e4000] video:0KiB audio:20840KiB subtitle:0KiB other streams:0KiB global headers:0KiB muxing overhead: 1.175908%
<span class="nv">size</span><span class="o">=</span>   21086KiB <span class="nb">time</span><span class="o">=</span>00:22:12.34 <span class="nv">bitrate</span><span class="o">=</span> 129.6kbits/s <span class="nv">speed</span><span class="o">=</span>43.8x
<span class="o">[</span>aac @ 0x151004f50] Qavg: 668.452
</code></pre></div></div>

<h2 id="easy-listening">Easy listening</h2>

<p>I’m using <a href="https://github.com/TortugaPower/BookPlayer">BookPlayer</a>, an open
source project to listen to books. I usually sync them by using AirDrop to send
the m4b files to my phone, then using its import menu.</p>

<hr />

<p>Happy listening!</p>

<p>P.S., I wrote
<a href="https://gist.github.com/cthoyt/62b26908b8822a7596c6b663ee910d5f">a script</a> that
automates crawling the Wind and Truth index, downloading, and converting these
files.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="reading" /><category term="automation" /><summary type="html"><![CDATA[Brandon Sanderson has been releasing a few chapters a week of his upcoming novel, Wind and Truth, on his publisher’s website leading up to its December 6th release. This includes the audiobook chapters, but they’re posted to Soundcloud and there’s no good way to listen at 1.6x speed. This post is a note sheet on how to download audio from Soundcloud and prepare it for my audiobook reader.]]></summary></entry><entry><title type="html">Dependency Groups and ReadTheDocs</title><link href="https://cthoyt.com/2024/11/19/rtfd-dependency-groups.html" rel="alternate" type="text/html" title="Dependency Groups and ReadTheDocs" /><published>2024-11-19T11:39:00+00:00</published><updated>2024-11-19T11:39:00+00:00</updated><id>https://cthoyt.com/2024/11/19/rtfd-dependency-groups</id><content type="html" xml:base="https://cthoyt.com/2024/11/19/rtfd-dependency-groups.html"><![CDATA[<p><a href="https://peps.python.org/pep-0735/">PEP 735</a> introduced dependency groups in
packaging metadata, which are complementary to
<a href="https://peps.python.org/pep-0631/">optional dependencies</a> in that they might
not correspond to features in the package, but rather be something like
development or release dependencies. I am slowly working towards
<a href="https://github.com/cthoyt/cookiecutter-snekpack/pull/32">updating</a> my
cookiecutter template
<a href="https://github.com/cthoyt/cookiecutter-snekpack">cookiecutter-snekpack</a> to use
PEP 735. So far, uv and tox have released support - all that’s left is
<a href="https://about.readthedocs.com/">ReadTheDocs</a>. This post summarizes the issue I
added to their issue tracker and the following discussion.</p>

<h2 id="summary-of-optional-dependencies-and-dependency-groups">Summary of optional dependencies and dependency groups.</h2>

<p>Before PEP 735, optional dependencies were often used both for extra features
(e.g., installing <code class="language-plaintext highlighter-rouge">lxml</code> for faster XML parsing) as well as development
dependencies (e.g., for testing and documentation build). Here’s what the syntax
for this looks like:</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[project.optional-dependencies]</span>
<span class="py">faster-xml</span> <span class="p">=</span> <span class="p">[</span>
    <span class="s">"lxml"</span><span class="p">,</span>
<span class="p">]</span>
<span class="py">tests</span> <span class="p">=</span> <span class="p">[</span>
    <span class="s">"pytest"</span><span class="p">,</span>
    <span class="s">"coverage"</span><span class="p">,</span>
<span class="p">]</span>
<span class="py">docs</span> <span class="p">=</span> <span class="p">[</span>
    <span class="py">"sphinx&gt;</span><span class="p">=</span><span class="mi">8</span><span class="s">",</span><span class="err">
</span>    <span class="py">"sphinx-rtd-theme&gt;</span><span class="p">=</span><span class="mf">3.0</span><span class="s">",</span><span class="err">
</span>    <span class="s">"sphinx-click"</span><span class="p">,</span>
    <span class="s">"sphinx_automodapi"</span><span class="p">,</span>
<span class="p">]</span>
</code></pre></div></div>

<p>When you’re using <code class="language-plaintext highlighter-rouge">pip</code> (or uv) to install, you can use square bracket notation
to say which ones go in.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>pip <span class="nb">install</span> .[tests,docs,faster-xml]
</code></pre></div></div>

<p>This is a bit problematic since it conflates what the purpose of optional
dependencies are. Some build tools tried to address with their on custom
configuration for development dependencies in <code class="language-plaintext highlighter-rouge">pyproject.toml</code> or elsewhere.
However, dependency groups gives us a more principled approach towards
categorizing dependencies that are not necessarily relevant for the code itself.
Now, we can split up the example from before like this:</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[project.optional-dependencies]</span>
<span class="py">faster-xml</span> <span class="p">=</span> <span class="p">[</span>
    <span class="s">"lxml"</span><span class="p">,</span>
<span class="p">]</span>

<span class="nn">[dependency-groups]</span>
<span class="py">tests</span> <span class="p">=</span> <span class="p">[</span>
    <span class="s">"pytest"</span><span class="p">,</span>
    <span class="s">"coverage"</span><span class="p">,</span>
<span class="p">]</span>
<span class="py">docs</span> <span class="p">=</span> <span class="p">[</span>
    <span class="py">"sphinx&gt;</span><span class="p">=</span><span class="mi">8</span><span class="s">",</span><span class="err">
</span>    <span class="py">"sphinx-rtd-theme&gt;</span><span class="p">=</span><span class="mf">3.0</span><span class="s">",</span><span class="err">
</span>    <span class="s">"sphinx_automodapi"</span><span class="p">,</span>
<span class="p">]</span>
</code></pre></div></div>

<p>When you’re using <code class="language-plaintext highlighter-rouge">pip</code> (or uv) to install, you can use <code class="language-plaintext highlighter-rouge">--dependency-groups</code> to
say which ones go in.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>pip <span class="nb">install</span> <span class="nt">--dependency-groups</span><span class="o">=</span>tests,typing .[faster-xml]
</code></pre></div></div>

<h2 id="preparing-an-environment-on-readthedocs">Preparing an environment on ReadTheDocs</h2>

<p>ReadTheDocs currently supports specifying optional dependencies (see
https://docs.readthedocs.io/en/stable/config-file/v2.html#packages) with
configuration like the following:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="m">2</span>

<span class="na">python</span><span class="pi">:</span>
  <span class="na">install</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">method</span><span class="pi">:</span> <span class="s">pip</span>
      <span class="na">path</span><span class="pi">:</span> <span class="s">.</span>
      <span class="na">extra_requirements</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">docs</span>
</code></pre></div></div>

<p>In my issue
<a href="https://github.com/readthedocs/readthedocs.org/issues/11766">#11766</a>, I
suggested defining an alternate key <code class="language-plaintext highlighter-rouge">dependency_groups</code> that could work the same
way.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="m">2</span>

<span class="na">python</span><span class="pi">:</span>
  <span class="na">install</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">method</span><span class="pi">:</span> <span class="s">pip</span>
      <span class="na">path</span><span class="pi">:</span> <span class="s">.</span>
      <span class="na">dependency_groups</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">docs</span>
</code></pre></div></div>

<h2 id="readthedocs-code-deep-dive-and-implementation-suggestion">ReadTheDocs code deep dive and implementation suggestion</h2>

<p>I found a few places that would be relevant for a theoretical implementation.
First, there’s a data structure that represents the slots available in the
configuration for <code class="language-plaintext highlighter-rouge">python &gt; install</code>. Here’s the code
(<a href="https://github.com/readthedocs/readthedocs.org/blob/404d82a448295c81a271c3143d0fc9c10a924555/readthedocs/config/models.py#L77-L82">permalink</a>):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">PythonInstall</span><span class="p">(</span><span class="n">Base</span><span class="p">):</span>
    <span class="n">__slots__</span> <span class="o">=</span> <span class="p">(</span>
        <span class="s">"path"</span><span class="p">,</span>
        <span class="s">"method"</span><span class="p">,</span>
        <span class="s">"extra_requirements"</span><span class="p">,</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>I’d simply add a new slot <code class="language-plaintext highlighter-rouge">dependency_groups</code> and update the related processing
code
(<a href="https://github.com/readthedocs/readthedocs.org/blob/404d82a448295c81a271c3143d0fc9c10a924555/readthedocs/doc_builder/python_environments.py#L66-L67">permalink</a>):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>
<span class="k">if</span> <span class="n">install</span><span class="p">.</span><span class="n">extra_requirements</span><span class="p">:</span>
    <span class="n">extra_req_param</span> <span class="o">=</span> <span class="s">"[{}]"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="s">","</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">install</span><span class="p">.</span><span class="n">extra_requirements</span><span class="p">))</span>
<span class="p">...</span>
</code></pre></div></div>

<p>Here’s what I’d do:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Added these next lines
</span><span class="n">dependency_group_args</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">if</span> <span class="n">install</span><span class="p">.</span><span class="n">dependency_groups</span><span class="p">:</span>
    <span class="c1"># not clear if the equals is necessary or if
</span>    <span class="c1"># this can be broken into two parts
</span>    <span class="n">dependency_group_args</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="s">"--dependency-groups={}"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="s">","</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">install</span><span class="p">.</span><span class="n">dependency_groups</span><span class="p">))</span>

<span class="n">extra_req_param</span> <span class="o">=</span> <span class="s">""</span>
<span class="k">if</span> <span class="n">install</span><span class="p">.</span><span class="n">extra_requirements</span><span class="p">:</span>
    <span class="n">extra_req_param</span> <span class="o">=</span> <span class="s">"[{}]"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="s">","</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">install</span><span class="p">.</span><span class="n">extra_requirements</span><span class="p">))</span>
<span class="bp">self</span><span class="p">.</span><span class="n">build_env</span><span class="p">.</span><span class="n">run</span><span class="p">(</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">venv_bin</span><span class="p">(</span><span class="n">filename</span><span class="o">=</span><span class="s">"python"</span><span class="p">),</span>
    <span class="s">"-m"</span><span class="p">,</span>
    <span class="s">"pip"</span><span class="p">,</span>
    <span class="s">"install"</span><span class="p">,</span>
    <span class="s">"--upgrade"</span><span class="p">,</span>
    <span class="s">"--upgrade-strategy"</span><span class="p">,</span>
    <span class="s">"only-if-needed"</span><span class="p">,</span>
    <span class="s">"--no-cache-dir"</span><span class="p">,</span>
    <span class="s">"{path}{extra_requirements}"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span>
        <span class="n">path</span><span class="o">=</span><span class="n">local_path</span><span class="p">,</span>
        <span class="n">extra_requirements</span><span class="o">=</span><span class="n">extra_req_param</span><span class="p">,</span>
    <span class="p">),</span>
    <span class="o">*</span><span class="n">dependency_group_args</span><span class="p">,</span>
    <span class="n">cwd</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">checkout_path</span><span class="p">,</span>
    <span class="n">bin_path</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">venv_bin</span><span class="p">(),</span>
<span class="p">)</span>
</code></pre></div></div>

<p>As a minor note, I would also do a bit of refactoring to store all the args into
the list and then splat all of them into <code class="language-plaintext highlighter-rouge">run()</code></p>

<h2 id="there-has-to-be-a-better-way-tm">There Has To Be A Better Way (TM)</h2>

<p>As Raymond H. always says, there has to be a better way. Turns out, the RTD team
is already working on a more generic way to override the installation command
via the configuration in
<a href="https://github.com/readthedocs/readthedocs.org/pull/11710">#11710</a>.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="m">2</span>

<span class="na">build</span><span class="pi">:</span>
  <span class="na">jobs</span><span class="pi">:</span>
    <span class="na">install</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">pip install --dependency-groups=tests,typing</span>
</code></pre></div></div>

<p>This reduces the need to make potentially lots of update to the rigid code I
worked on above. This makes me very happy!</p>

<hr />

<p>I’ve received a lot of poorly written issues and requests in my open source work
(see the PyKEEN issue tracker) so I thought it was incredibly important to write
this issue very well. It occurred to me that the thought process here is also
maybe generally interesting, so that’s why I copied it to my blog.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="Python" /><category term="packaging" /><category term="Cookiecutter" /><category term="documentation" /><summary type="html"><![CDATA[PEP 735 introduced dependency groups in packaging metadata, which are complementary to optional dependencies in that they might not correspond to features in the package, but rather be something like development or release dependencies. I am slowly working towards updating my cookiecutter template cookiecutter-snekpack to use PEP 735. So far, uv and tox have released support - all that’s left is ReadTheDocs. This post summarizes the issue I added to their issue tracker and the following discussion.]]></summary></entry><entry><title type="html">Building Graphviz when installing PyGraphviz</title><link href="https://cthoyt.com/2024/11/05/installing-pygraphviz.html" rel="alternate" type="text/html" title="Building Graphviz when installing PyGraphviz" /><published>2024-11-05T06:48:00+00:00</published><updated>2024-11-05T06:48:00+00:00</updated><id>https://cthoyt.com/2024/11/05/installing-pygraphviz</id><content type="html" xml:base="https://cthoyt.com/2024/11/05/installing-pygraphviz.html"><![CDATA[<p><a href="https://graphviz.org">Graphviz</a> is software for graph visualization written in
C. <a href="https://pypi.org/project/pygraphviz">PyGraphviz</a> provides a nice Python
wrapper for it. The issue is that getting Python to know about the C headers
changes every few months. I’ll try and keep this blog post updated every time
there are some changes.</p>

<h1 id="november-2024">November 2024</h1>

<p>These days, <code class="language-plaintext highlighter-rouge">pip install graphviz</code> seemed to work but <code class="language-plaintext highlighter-rouge">uv pip install graphviz</code>
gave the following:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>uv pip <span class="nb">install </span>pygraphviz
<span class="go">Using Python 3.12.7 environment at /Users/cthoyt/.virtualenvs/orcid
Resolved 1 package in 597ms
error: Failed to prepare distributions
  Caused by: Failed to download and build `pygraphviz==1.14`
  Caused by: Build backend failed to build wheel through `build_wheel` (exit status: 1)

[stdout]
running bdist_wheel
running build
running build_py
creating build/lib.macosx-14.0-arm64-cpython-312/pygraphviz
</span><span class="gp">copying pygraphviz/scraper.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz
<span class="gp">copying pygraphviz/graphviz.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz
<span class="gp">copying pygraphviz/__init__.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz
<span class="gp">copying pygraphviz/agraph.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz
<span class="gp">copying pygraphviz/testing.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz
<span class="go">creating build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
</span><span class="gp">copying pygraphviz/tests/test_unicode.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
<span class="gp">copying pygraphviz/tests/test_scraper.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
<span class="gp">copying pygraphviz/tests/test_readwrite.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
<span class="gp">copying pygraphviz/tests/test_string.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
<span class="gp">copying pygraphviz/tests/__init__.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
<span class="gp">copying pygraphviz/tests/test_html.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
<span class="gp">copying pygraphviz/tests/test_node_attributes.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
<span class="gp">copying pygraphviz/tests/test_drawing.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
<span class="gp">copying pygraphviz/tests/test_repr_mimebundle.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
<span class="gp">copying pygraphviz/tests/test_subgraph.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
<span class="gp">copying pygraphviz/tests/test_close.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
<span class="gp">copying pygraphviz/tests/test_edge_attributes.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
<span class="gp">copying pygraphviz/tests/test_clear.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
<span class="gp">copying pygraphviz/tests/test_layout.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
<span class="gp">copying pygraphviz/tests/test_attribute_defaults.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
<span class="gp">copying pygraphviz/tests/test_graph.py -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz/tests
<span class="go">running egg_info
writing pygraphviz.egg-info/PKG-INFO
writing dependency_links to pygraphviz.egg-info/dependency_links.txt
writing top-level names to pygraphviz.egg-info/top_level.txt
reading manifest file 'pygraphviz.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
adding license file 'LICENSE'
writing manifest file 'pygraphviz.egg-info/SOURCES.txt'
</span><span class="gp">copying pygraphviz/graphviz.i -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz
<span class="gp">copying pygraphviz/graphviz_wrap.c -&gt;</span><span class="w"> </span>build/lib.macosx-14.0-arm64-cpython-312/pygraphviz
<span class="go">running build_ext
building 'pygraphviz._graphviz' extension
creating build/temp.macosx-14.0-arm64-cpython-312/pygraphviz
clang -fno-strict-overflow -Wsign-compare -Wunreachable-code -fno-common -dynamic -DNDEBUG -g -O3 -Wall -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk -DSWIG_PYTHON_STRICT_BYTE_CHAR -I/Users/cthoyt/Library/Caches/uv/builds-v0/.tmpKw7zNP/include -I/opt/homebrew/opt/python@3.12/Frameworks/Python.framework/Versions/3.12/include/python3.12 -c pygraphviz/graphviz_wrap.c -o build/temp.macosx-14.0-arm64-cpython-312/pygraphviz/graphviz_wrap.o

[stderr]
warning: no files found matching '*.swg'
warning: no files found matching '*.png' under directory 'doc'
warning: no files found matching '*.html' under directory 'doc'
warning: no files found matching '*.txt' under directory 'doc'
warning: no files found matching '*.css' under directory 'doc'
warning: no previously-included files matching '*~' found anywhere in distribution
warning: no previously-included files matching '*.pyc' found anywhere in distribution
warning: no previously-included files matching '.svn' found anywhere in distribution
no previously-included directories found matching 'doc/build'
pygraphviz/graphviz_wrap.c:9:9: warning: 'SWIG_PYTHON_STRICT_BYTE_CHAR' macro redefined [-Wmacro-redefined]
</span><span class="gp">#</span>define SWIG_PYTHON_STRICT_BYTE_CHAR
<span class="go">        ^
</span><span class="gp">&lt;command line&gt;</span>:2:9: note: previous definition is here
<span class="gp">#</span>define SWIG_PYTHON_STRICT_BYTE_CHAR 1
<span class="go">        ^
pygraphviz/graphviz_wrap.c:3023:10: fatal error: 'graphviz/cgraph.h' file not found
</span><span class="gp">#</span>include <span class="s2">"graphviz/cgraph.h"</span>
<span class="go">         ^~~~~~~~~~~~~~~~~~~
1 warning and 1 error generated.
error: command '/usr/bin/clang' failed with exit code 1
  Caused by: This error likely indicates that you need to install a library that provides "graphviz/cgraph.h" for pygraphviz@1.14
</span></code></pre></div></div>

<p>Here’s one solution that work (of the many possible ones):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">export</span> <span class="n">CFLAGS</span><span class="o">=</span><span class="s">"-I$(brew --prefix graphviz)/include"</span>
<span class="err">$</span> <span class="n">export</span> <span class="n">LDFLAGS</span><span class="o">=</span><span class="s">"-L$(brew --prefix graphviz)/lib"</span>
<span class="err">$</span> <span class="n">uv</span> <span class="n">pip</span> <span class="n">install</span> <span class="n">pygraphviz</span>
<span class="n">Using</span> <span class="n">Python</span> <span class="mf">3.12</span><span class="p">.</span><span class="mi">7</span> <span class="n">environment</span> <span class="n">at</span> <span class="o">/</span><span class="n">Users</span><span class="o">/</span><span class="n">cthoyt</span><span class="o">/</span><span class="p">.</span><span class="n">virtualenvs</span><span class="o">/</span><span class="n">orcid</span>
<span class="n">Resolved</span> <span class="mi">1</span> <span class="n">package</span> <span class="ow">in</span> <span class="mi">2</span><span class="n">ms</span>
   <span class="n">Built</span> <span class="n">pygraphviz</span><span class="o">==</span><span class="mf">1.14</span>
<span class="n">Prepared</span> <span class="mi">1</span> <span class="n">package</span> <span class="ow">in</span> <span class="mi">908</span><span class="n">ms</span>
<span class="n">Installed</span> <span class="mi">1</span> <span class="n">package</span> <span class="ow">in</span> <span class="mi">1</span><span class="n">ms</span>
 <span class="o">+</span> <span class="n">pygraphviz</span><span class="o">==</span><span class="mf">1.14</span>
</code></pre></div></div>

<p>There’s also talk of using <code class="language-plaintext highlighter-rouge">--global-option</code> to pass
<code class="language-plaintext highlighter-rouge">-I/opt/homebrew/opt/graphviz/include</code> and <code class="language-plaintext highlighter-rouge">-L/opt/homebrew/opt/graphviz/lib</code>,
but I couldn’t figure this out for <code class="language-plaintext highlighter-rouge">uv pip install</code>.</p>

<h1 id="2023-era">2023 Era</h1>

<p>I was on Python 3.10 on a M2 Mac with macOS 13. <code class="language-plaintext highlighter-rouge">pygraphviz</code> didn’t have a
pre-built wheel for my systen so <code class="language-plaintext highlighter-rouge">python -m pip install pygraphviz</code> gave the
following error:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>python <span class="nt">-m</span> pip <span class="nb">install </span>pygraphviz
<span class="c">...
</span><span class="go">clang -Wno-unused-result -Wsign-compare -Wunreachable-code -fno-common -dynamic -DNDEBUG -g -fwrapv -O3 -Wall -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX13.sdk -DSWIG_PYTHON_STRICT_BYTE_CHAR -I/Users/cthoyt/.virtualenvs/indra/include -I/usr/local/opt/python@3.10/Frameworks/Python.framework/Versions/3.10/include/python3.10 -c pygraphviz/graphviz_wrap.c -o build/temp.macosx-13-x86_64-cpython-310/pygraphviz/graphviz_wrap.o
      pygraphviz/graphviz_wrap.c:3020:10: fatal error: 'graphviz/cgraph.h' file not found
</span><span class="gp">      #</span>include <span class="s2">"graphviz/cgraph.h"</span>
<span class="go">               ^~~~~~~~~~~~~~~~~~~
      1 error generated.
      error: command '/usr/bin/clang' failed with exit code 1
      [end of output]
</span></code></pre></div></div>

<p>I figured out based on the Homebrew troubleshooting in the docs
(https://pygraphviz.github.io/documentation/stable/install.html#homebrew) that
the following works:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python <span class="nt">-m</span> pip <span class="nb">install</span> <span class="nt">--use-pep517</span> <span class="se">\</span>
    <span class="nt">--config-setting</span><span class="o">=</span><span class="s2">"--global-option=build_ext"</span> <span class="se">\</span>
    <span class="nt">--config-setting</span><span class="o">=</span><span class="s2">"--build-option=-I</span><span class="si">$(</span>brew <span class="nt">--prefix</span> graphviz<span class="si">)</span><span class="s2">/include/"</span> <span class="se">\</span>
    <span class="nt">--config-setting</span><span class="o">=</span><span class="s2">"--build-option=-L</span><span class="si">$(</span>brew <span class="nt">--prefix</span> graphviz<span class="si">)</span><span class="s2">/lib/"</span> <span class="se">\</span>
    pygraphviz
</code></pre></div></div>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="Python" /><category term="GraphViz" /><category term="environments" /><summary type="html"><![CDATA[Graphviz is software for graph visualization written in C. PyGraphviz provides a nice Python wrapper for it. The issue is that getting Python to know about the C headers changes every few months. I’ll try and keep this blog post updated every time there are some changes.]]></summary></entry><entry><title type="html">Some Haskell I Tried to Write</title><link href="https://cthoyt.com/2024/09/26/some-haskell.html" rel="alternate" type="text/html" title="Some Haskell I Tried to Write" /><published>2024-09-26T10:49:00+00:00</published><updated>2024-09-26T10:49:00+00:00</updated><id>https://cthoyt.com/2024/09/26/some-haskell</id><content type="html" xml:base="https://cthoyt.com/2024/09/26/some-haskell.html"><![CDATA[<p>I’m working through making a contribution to
<a href="https://github.com/jgm/pandoc">pandoc</a> that adds first-class support for author
role annotations using the
<a href="https://credit.niso.org/contributor-roles">Contribution Role Taxonomy (CRediT)</a>
and also outputs compliant
<a href="https://jats.nlm.nih.gov/publishing/">Journal Publishing Tag Set (JATS)</a> XML.
This has lead me down a (losing) journey with learning the Haskell programming
language, so I thought I would post a short note on a function I tried to
understand.</p>

<p>For some context, the first stream of changes I sent were in
<a href="https://github.com/jgm/pandoc/pull/10153">jgm/pandoc #10153</a>. I have done some
in-place squashes on the git history, so apologies to future readers if this
isn’t a helpful thread. I appreciated the help from Pandoc’s maintainer John
MacFarlane, but he suggested I add the following code and I just don’t know
enough Haskell to make sense of it:</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">addCreditName</span> <span class="o">::</span> <span class="kt">M</span><span class="o">.</span><span class="kt">Map</span> <span class="kt">Text</span> <span class="kt">Text</span> <span class="o">-&gt;</span> <span class="kt">M</span><span class="o">.</span><span class="kt">Map</span> <span class="kt">Text</span> <span class="kt">Text</span>
<span class="n">addCreditName</span> <span class="n">rolemap</span> <span class="o">=</span>
  <span class="kr">case</span> <span class="kt">M</span><span class="o">.</span><span class="n">lookup</span> <span class="s">"credit-name"</span> <span class="n">rolemap</span> <span class="kr">of</span>
    <span class="kt">Just</span> <span class="kr">_</span> <span class="o">-&gt;</span> <span class="n">rolemap</span>
    <span class="kt">Nothing</span> <span class="o">-&gt;</span> <span class="n">maybe</span> <span class="n">id</span> <span class="p">(</span><span class="kt">M</span><span class="o">.</span><span class="n">insert</span> <span class="s">"credit-name"</span><span class="p">)</span>
      <span class="p">(</span><span class="kt">M</span><span class="o">.</span><span class="n">lookup</span> <span class="s">"credit-id"</span> <span class="n">rolemap</span> <span class="o">&gt;&gt;=</span> <span class="n">flip</span> <span class="kt">M</span><span class="o">.</span><span class="n">lookup</span> <span class="n">creditNames</span><span class="p">)</span>
</code></pre></div></div>

<p>The goal was actually pretty simple. I have a dictionary that maybe has a
<code class="language-plaintext highlighter-rouge">credit-name</code> key. If it does, then we’re done. If not, and it has a <code class="language-plaintext highlighter-rouge">credit-id</code>
key, get the value out of that and look up the <code class="language-plaintext highlighter-rouge">credit-name</code> using an external
<code class="language-plaintext highlighter-rouge">creditNames</code> dictionary. The problem is, I can’t understand this without going
on a massive deep-dive on Haskell to actually understand the way Haskell
programs treat function calls, monads (which is a yo-dawg in the category of
jokes), and function polymorphisms.</p>

<p>One thing to keep in mind is that Haskell is a functional programming language
and everything is supposed to be immutable. This means that mapping insertion
operations (<code class="language-plaintext highlighter-rouge">M.insert</code>) are returning a new dictionary. That made it possible to
at least try a more verbose way of doing what I needed to.</p>

<p>I like Haskell’s <code class="language-plaintext highlighter-rouge">case</code> statement and first-class notion of optionals, so I
thought I’d try re-writing this code from above in a bit more straight-forward
way:</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">addCreditName</span> <span class="o">::</span> <span class="kt">M</span><span class="o">.</span><span class="kt">Map</span> <span class="kt">Text</span> <span class="kt">Text</span> <span class="o">-&gt;</span> <span class="kt">M</span><span class="o">.</span><span class="kt">Map</span> <span class="kt">Text</span> <span class="kt">Text</span>
<span class="n">addCreditName</span> <span class="n">role</span> <span class="o">=</span>
  <span class="c1">-- Try looking if there's a "credit-name" key in the role dictionary</span>
  <span class="kr">case</span> <span class="kt">M</span><span class="o">.</span><span class="n">lookup</span> <span class="s">"credit-name"</span> <span class="n">role</span> <span class="kr">of</span>
    <span class="c1">-- If there's already an explicitly specified "credit-name"</span>
    <span class="c1">-- key in the role dictionary, then we don't have to do anything</span>
    <span class="kt">Just</span> <span class="kr">_</span> <span class="o">-&gt;</span> <span class="n">role</span>
    <span class="kt">Nothing</span> <span class="o">-&gt;</span>
      <span class="kr">case</span> <span class="kt">M</span><span class="o">.</span><span class="n">lookup</span> <span class="s">"credit-id"</span> <span class="n">role</span> <span class="kr">of</span>
        <span class="c1">-- If there isn't already a "credit-id" key in the role</span>
        <span class="c1">-- dictionary, then we aren't able to do anything</span>
        <span class="kt">Nothing</span> <span class="o">-&gt;</span> <span class="n">role</span>
        <span class="kt">Just</span> <span class="n">creditIdentifier</span> <span class="o">-&gt;</span>
          <span class="c1">-- Try looking up the value from the "credit-id" key, which</span>
          <span class="c1">-- we stored in the `creditIdentifier` variable, is in the</span>
          <span class="c1">-- creditNames dictionary, which is defined as a constant above</span>
          <span class="kr">case</span> <span class="kt">M</span><span class="o">.</span><span class="n">lookup</span> <span class="n">creditIdentifier</span> <span class="n">creditNames</span> <span class="kr">of</span>
            <span class="c1">-- If the credit-id value from the role dictionary is not</span>
            <span class="c1">-- in the creditNames lookup dictionary, then we can't do anything</span>
            <span class="kt">Nothing</span> <span class="o">-&gt;</span> <span class="n">role</span>
            <span class="c1">-- If the credit-id value from the role dictionary is in</span>
            <span class="c1">-- the creditNames lookup dictionary, insert it back into the</span>
            <span class="c1">-- role dictionary under the "credit-name" key and return</span>
            <span class="kt">Just</span> <span class="n">creditName</span> <span class="o">-&gt;</span> <span class="kt">M</span><span class="o">.</span><span class="n">insert</span> <span class="s">"credit-name"</span> <span class="n">creditName</span> <span class="n">role</span>
</code></pre></div></div>

<p>The funny thing is, I have been called out before on increasing confusion by
getting tricky when writing Python code. I guess I had it coming!</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="Haskell" /><category term="JATS" /><category term="publishing" /><category term="Pandoc" /><summary type="html"><![CDATA[I’m working through making a contribution to pandoc that adds first-class support for author role annotations using the Contribution Role Taxonomy (CRediT) and also outputs compliant Journal Publishing Tag Set (JATS) XML. This has lead me down a (losing) journey with learning the Haskell programming language, so I thought I would post a short note on a function I tried to understand.]]></summary></entry><entry><title type="html">Programmatic Access to a Wordpress User List</title><link href="https://cthoyt.com/2024/09/20/wordpress-api.html" rel="alternate" type="text/html" title="Programmatic Access to a Wordpress User List" /><published>2024-09-20T10:31:00+00:00</published><updated>2024-09-20T10:31:00+00:00</updated><id>https://cthoyt.com/2024/09/20/wordpress-api</id><content type="html" xml:base="https://cthoyt.com/2024/09/20/wordpress-api.html"><![CDATA[<p>The <a href="https://www.biocuration.org/">International Society of Biocuration (ISB)</a>
partners with the journal <a href="https://academic.oup.com/database">Database</a> to get
discounts for its members when they publish there. This means the ISB’s
executive committee needs to send a member list to the journal’s editor.
Historically, this has been done manually by exporting the list from the
membership management plugin in the ISB Wordpress blog once per month and
emailing it to th This post is about my journey trying to automate it</p>

<h1 id="1-there-must-be-an-api-for-this">1. There must be an API for this</h1>

<p>Wordpress has a <a href="https://developer.wordpress.org/rest-api/">programmatic API</a>,
and specifically an endpoint to list
<a href="https://developer.wordpress.org/rest-api/reference/users/">users</a>.</p>

<p>After logging into the ISB’s Wordpress site, I was able to list users by
navigating to the endpoint in browser
https://biocuration.org/wp-json/wp/v2/users. Note: this page won’t work for you
unless you’re on the EC and have admin powers. I wanted to replicate accessing
this page through a Python script, so I was suggested by the
<a href="https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/">official Wordpress documentation</a>
to create an application password and then use simple HTTP authentication. The
docs said try this, after replacing the username with the Wordpress account (for
me, that’s <code class="language-plaintext highlighter-rouge">cthoyt</code>) and the application password, which is a string with the
form of <code class="language-plaintext highlighter-rouge">XXXX XXXX XXXX XXXX XXXX XXXX</code>. The documentation suggested doing the
following curl command, which works:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">--user</span> <span class="s2">"USERNAME:PASSWORD"</span> https://biocuration.org/wp-json/wp/v2/users
</code></pre></div></div>

<p>If you want to translate this to Python, it looks like this.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pystow</span><span class="p">,</span> <span class="n">base64</span><span class="p">,</span> <span class="n">requests</span>

<span class="n">username</span> <span class="o">=</span> <span class="n">pystow</span><span class="p">.</span><span class="n">get_config</span><span class="p">(</span><span class="s">"isb"</span><span class="p">,</span> <span class="s">"wordpress_username"</span><span class="p">,</span> <span class="n">raise_on_missing</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">password</span> <span class="o">=</span> <span class="n">pystow</span><span class="p">.</span><span class="n">get_config</span><span class="p">(</span><span class="s">"isb"</span><span class="p">,</span> <span class="s">"wordpress_password"</span><span class="p">,</span> <span class="n">raise_on_missing</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="n">token</span> <span class="o">=</span> <span class="n">base64</span><span class="p">.</span><span class="n">b64encode</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">username</span><span class="si">}</span><span class="s">:</span><span class="si">{</span><span class="n">password</span><span class="si">}</span><span class="s">"</span><span class="p">.</span><span class="n">encode</span><span class="p">()).</span><span class="n">decode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">)</span>

<span class="n">res</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span>
    <span class="s">'https://www.biocuration.org/wp-json/wp/v2/users'</span><span class="p">,</span>
    <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s">"Authorization"</span><span class="p">:</span> <span class="sa">f</span><span class="s">"Basic </span><span class="si">{</span><span class="n">token</span><span class="si">}</span><span class="s">"</span><span class="p">,</span> <span class="s">"user-agent"</span><span class="p">:</span> <span class="s">"curl"</span><span class="p">},</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Luckily, encoding the username/password string with basic authorization is such
a common pattern that the <code class="language-plaintext highlighter-rouge">request</code> (and any other modern library) has a nice
par for this.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pystow</span><span class="p">,</span> <span class="n">requests</span>

<span class="n">username</span> <span class="o">=</span> <span class="n">pystow</span><span class="p">.</span><span class="n">get_config</span><span class="p">(</span><span class="s">"isb"</span><span class="p">,</span> <span class="s">"wordpress_username"</span><span class="p">,</span> <span class="n">raise_on_missing</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">password</span> <span class="o">=</span> <span class="n">pystow</span><span class="p">.</span><span class="n">get_config</span><span class="p">(</span><span class="s">"isb"</span><span class="p">,</span> <span class="s">"wordpress_password"</span><span class="p">,</span> <span class="n">raise_on_missing</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="n">res</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span>
    <span class="s">'https://www.biocuration.org/wp-json/wp/v2/users'</span><span class="p">,</span>
    <span class="n">auth</span><span class="o">=</span><span class="p">(</span><span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="p">),</span>
    <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s">"user-agent"</span><span class="p">:</span> <span class="s">"curl"</span><span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>

<p>I burned a ton of time with this because it turns out that Wordpress blocks both
the user agents for the <code class="language-plaintext highlighter-rouge">request</code> and <code class="language-plaintext highlighter-rouge">httpx</code> libraries. Therefore, you have to
explicitly set the <code class="language-plaintext highlighter-rouge">user-agent</code> header to something else, or you get a HTTP 403
forbidden error.</p>

<h1 id="2-the-api-doesnt-do-what-i-wanted">2. The API doesn’t do what I wanted</h1>

<p>It turns out that by default, all the users that never made a post get filtered
out. This is bad since the only users who are making posts on the ISB are the
handful who have or are currently on the executive committee.</p>

<p>So, obviously, the next step is to start injecting in PHP code to change how the
API works. I found a
<a href="https://github.com/WP-API/WP-API/issues/2300#issuecomment-299202391">comment</a>
from Tim Jensen, a Wordpress developer, that suggests adding the following hooks
into the Wordpress theme’s <code class="language-plaintext highlighter-rouge">functions.php</code> file by navigating to
https://www.biocuration.org/wp-admin/theme-editor.php?file=functions.php&amp;theme=executive:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="n">remove_has_published_posts_from_api_user_query</span><span class="p">(</span><span class="nv">$prepared_args</span><span class="p">,</span> <span class="nv">$request</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">unset</span><span class="p">(</span><span class="nv">$prepared_args</span><span class="p">[</span><span class="s1">'has_published_posts'</span><span class="p">]);</span>
    <span class="k">return</span> <span class="nv">$prepared_args</span><span class="p">;</span>
<span class="p">}</span>
<span class="nf">add_filter</span><span class="p">(</span><span class="s1">'rest_user_query'</span><span class="p">,</span> <span class="s1">'remove_has_published_posts_from_api_user_query'</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">2</span><span class="p">);</span>
</code></pre></div></div>

<p>This actually worked! But, it wasn’t the end of the story.</p>

<h1 id="3-full-names-are-part-of-the-plugins-metadata">3. Full names are part of the plugin’s metadata</h1>

<p>It turns out that the user data model isn’t all that comprehensive in Wordpress.
What we really needed was the full name and email address for each person, and
that was stored in an auxillary SQL table created and managed by the memberships
plugin. Note that the plugin lives on top of Wordpress’s first-party users list,
and doesn’t modify the API access to the user list.</p>

<p>So obviously, the next step was to add new API endpoints. I learned PHP at the
ripe age of 14 and when I learned some other languages, I never looked back, so
this wasn’t so easy. Plus, Wordpress has a huge set of functions and idioms on
top. With a little help from ChatGPT, I was able to write a new API endpoint to
add to <code class="language-plaintext highlighter-rouge">functions.php</code> that got users whose membership expiration date is in the
future and joined in the metadata for their first and last names:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="n">export_pmp_members_to_csv</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">global</span> <span class="nv">$wpdb</span><span class="p">;</span>

    <span class="c1">// Set headers to make the response downloadable as a CSV file</span>
    <span class="nb">header</span><span class="p">(</span><span class="s1">'Content-Type: text/csv'</span><span class="p">);</span>
    <span class="nb">header</span><span class="p">(</span><span class="s1">'Content-Disposition: attachment; filename="members_export.csv";'</span><span class="p">);</span>
    <span class="nb">header</span><span class="p">(</span><span class="s1">'Pragma: no-cache'</span><span class="p">);</span>
    <span class="nb">header</span><span class="p">(</span><span class="s1">'Expires: 0'</span><span class="p">);</span>

    <span class="c1">// Open output stream (in-memory file)</span>
    <span class="nv">$output</span> <span class="o">=</span> <span class="nb">fopen</span><span class="p">(</span><span class="s1">'php://output'</span><span class="p">,</span> <span class="s1">'w'</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$output</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nc">WP_Error</span><span class="p">(</span><span class="s1">'file_error'</span><span class="p">,</span> <span class="s1">'Unable to generate CSV file.'</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">// Add CSV header row</span>
    <span class="nb">fputcsv</span><span class="p">(</span><span class="nv">$output</span><span class="p">,</span> <span class="p">[</span><span class="s1">'id'</span><span class="p">,</span> <span class="s1">'first'</span><span class="p">,</span> <span class="s1">'last'</span><span class="p">,</span> <span class="s1">'email'</span><span class="p">,</span> <span class="s1">'level'</span><span class="p">,</span> <span class="s1">'start'</span><span class="p">,</span> <span class="s1">'ends'</span><span class="p">]);</span>

    <span class="c1">// Query to get PMP members and their membership details</span>
    <span class="nv">$results</span> <span class="o">=</span> <span class="nv">$wpdb</span><span class="o">-&gt;</span><span class="nf">get_results</span><span class="p">(</span><span class="s2">"
        SELECT
            u.ID,
            um_first_name.meta_value AS first_name,
            um_last_name.meta_value AS last_name,
            u.user_email, ml.name as membership_name,
            mu.startdate,
            mu.enddate
        FROM </span><span class="si">{</span><span class="nv">$wpdb</span><span class="o">-&gt;</span><span class="n">users</span><span class="si">}</span><span class="s2"> u
        LEFT JOIN </span><span class="si">{</span><span class="nv">$wpdb</span><span class="o">-&gt;</span><span class="n">prefix</span><span class="si">}</span><span class="s2">pmpro_memberships_users mu ON u.ID = mu.user_id
        LEFT JOIN </span><span class="si">{</span><span class="nv">$wpdb</span><span class="o">-&gt;</span><span class="n">prefix</span><span class="si">}</span><span class="s2">pmpro_membership_levels ml ON mu.membership_id = ml.id
        LEFT JOIN </span><span class="si">{</span><span class="nv">$wpdb</span><span class="o">-&gt;</span><span class="n">prefix</span><span class="si">}</span><span class="s2">usermeta um_first_name ON u.ID = um_first_name.user_id
            AND um_first_name.meta_key = 'first_name'
        LEFT JOIN </span><span class="si">{</span><span class="nv">$wpdb</span><span class="o">-&gt;</span><span class="n">prefix</span><span class="si">}</span><span class="s2">usermeta um_last_name ON u.ID = um_last_name.user_id
            AND um_last_name.meta_key = 'last_name'
        WHERE mu.membership_id IS NOT NULL
            AND (mu.enddate &gt; CURDATE() OR mu.enddate IS NULL)
    "</span><span class="p">);</span>

    <span class="c1">// Loop through results and write each row to the CSV</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="nv">$results</span> <span class="k">as</span> <span class="nv">$row</span><span class="p">)</span> <span class="p">{</span>
        <span class="nb">fputcsv</span><span class="p">(</span><span class="nv">$output</span><span class="p">,</span> <span class="p">[</span>
            <span class="nv">$row</span><span class="o">-&gt;</span><span class="no">ID</span><span class="p">,</span>
            <span class="nv">$row</span><span class="o">-&gt;</span><span class="n">first_name</span><span class="p">,</span>
            <span class="nv">$row</span><span class="o">-&gt;</span><span class="n">last_name</span><span class="p">,</span>
            <span class="nv">$row</span><span class="o">-&gt;</span><span class="n">user_email</span><span class="p">,</span>
            <span class="nv">$row</span><span class="o">-&gt;</span><span class="n">membership_name</span><span class="p">,</span>
            <span class="nv">$row</span><span class="o">-&gt;</span><span class="n">startdate</span><span class="p">,</span>
            <span class="nv">$row</span><span class="o">-&gt;</span><span class="n">enddate</span>
        <span class="p">]);</span>
    <span class="p">}</span>

    <span class="c1">// Close the output stream</span>
    <span class="nb">fclose</span><span class="p">(</span><span class="nv">$output</span><span class="p">);</span>

    <span class="c1">// Terminate script to ensure no extra output is sent</span>
    <span class="k">exit</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">function</span> <span class="n">register_pmp_export_route</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// the actual path to the endpoint isn't important here</span>
    <span class="nf">register_rest_route</span><span class="p">(</span><span class="s1">'path/to/endpoint/'</span><span class="p">,</span> <span class="s1">'/export-members'</span><span class="p">,</span> <span class="p">[</span>
        <span class="s1">'methods'</span> <span class="o">=&gt;</span> <span class="s1">'GET'</span><span class="p">,</span>
        <span class="s1">'callback'</span> <span class="o">=&gt;</span> <span class="s1">'export_pmp_members_to_csv'</span>
        <span class="s1">'permission_callback'</span> <span class="o">=&gt;</span> <span class="k">function</span><span class="p">()</span> <span class="p">{</span>
            <span class="c1">// Optionally, restrict access to logged-in users with specific capabilities</span>
            <span class="k">return</span> <span class="nf">current_user_can</span><span class="p">(</span><span class="s1">'manage_options'</span><span class="p">);</span> <span class="c1">// Restrict to admin users</span>
        <span class="p">}</span>
    <span class="p">]);</span>
<span class="p">}</span>
<span class="nf">add_action</span><span class="p">(</span><span class="s1">'rest_api_init'</span><span class="p">,</span> <span class="s1">'register_pmp_export_route'</span><span class="p">);</span>
</code></pre></div></div>

<h1 id="4-automate-it">4. Automate it</h1>

<p>Why stop at just being able to export the sheet? I wanted to go another mile and
make sure that Wordpress sends an email to the right person at the journal on a
monthly basis.</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">/</span> <span class="k">Function</span> <span class="n">to</span> <span class="n">generate</span> <span class="n">and</span> <span class="n">send</span> <span class="n">the</span> <span class="n">CSV</span> <span class="n">via</span> <span class="n">email</span>
<span class="n">function</span> <span class="n">send_pmp_members_csv_via_email</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">global</span> <span class="nv">$wpdb</span><span class="p">;</span>

    <span class="c1">// Create a temporary file to store the CSV</span>
    <span class="nv">$tmp_file</span> <span class="o">=</span> <span class="nb">tempnam</span><span class="p">(</span><span class="nb">sys_get_temp_dir</span><span class="p">(),</span> <span class="s1">'pmp_csv_'</span><span class="p">)</span> <span class="mf">.</span> <span class="s1">'.csv'</span><span class="p">;</span>
    <span class="nv">$output</span> <span class="o">=</span> <span class="nb">fopen</span><span class="p">(</span><span class="nv">$tmp_file</span><span class="p">,</span> <span class="s1">'w'</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$output</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span><span class="p">;</span> <span class="c1">// Handle error appropriately</span>
    <span class="p">}</span>

    <span class="c1">// Add CSV header row</span>
    <span class="nb">fputcsv</span><span class="p">(</span><span class="nv">$output</span><span class="p">,</span> <span class="p">[</span><span class="s1">'ID'</span><span class="p">,</span> <span class="s1">'Username'</span><span class="p">,</span> <span class="s1">'First Name'</span><span class="p">,</span> <span class="s1">'Last Name'</span><span class="p">,</span> <span class="s1">'Email'</span><span class="p">,</span> <span class="s1">'Membership Level Name'</span><span class="p">,</span> <span class="s1">'Joined Date'</span><span class="p">,</span> <span class="s1">'Expires Date'</span><span class="p">]);</span>

    <span class="c1">// Query to get PMP members and their membership details</span>
    <span class="nv">$results</span> <span class="o">=</span> <span class="nv">$wpdb</span><span class="o">-&gt;</span><span class="nf">get_results</span><span class="p">(</span><span class="s2">"
        SELECT u.ID,
            u.user_login,
            um_first_name.meta_value AS first_name,
            um_last_name.meta_value AS last_name,
            u.user_email,
            ml.name as membership_name
            mu.startdate,
            mu.enddate,
        FROM </span><span class="si">{</span><span class="nv">$wpdb</span><span class="o">-&gt;</span><span class="n">users</span><span class="si">}</span><span class="s2"> u
        LEFT JOIN </span><span class="si">{</span><span class="nv">$wpdb</span><span class="o">-&gt;</span><span class="n">prefix</span><span class="si">}</span><span class="s2">pmpro_memberships_users mu ON u.ID = mu.user_id
        LEFT JOIN </span><span class="si">{</span><span class="nv">$wpdb</span><span class="o">-&gt;</span><span class="n">prefix</span><span class="si">}</span><span class="s2">pmpro_membership_levels ml ON mu.membership_id = ml.id
        LEFT JOIN </span><span class="si">{</span><span class="nv">$wpdb</span><span class="o">-&gt;</span><span class="n">prefix</span><span class="si">}</span><span class="s2">usermeta um_first_name ON u.ID = um_first_name.user_id
            AND um_first_name.meta_key = 'first_name'
        LEFT JOIN </span><span class="si">{</span><span class="nv">$wpdb</span><span class="o">-&gt;</span><span class="n">prefix</span><span class="si">}</span><span class="s2">usermeta um_last_name ON u.ID = um_last_name.user_id
            AND um_last_name.meta_key = 'last_name'
        WHERE mu.membership_id IS NOT NULL
        AND (mu.enddate &gt; CURDATE() OR mu.enddate IS NULL)
    "</span><span class="p">);</span>

    <span class="c1">// Write data to CSV</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="nv">$results</span> <span class="k">as</span> <span class="nv">$row</span><span class="p">)</span> <span class="p">{</span>
        <span class="nb">fputcsv</span><span class="p">(</span><span class="nv">$output</span><span class="p">,</span> <span class="p">[</span>
            <span class="nv">$row</span><span class="o">-&gt;</span><span class="no">ID</span><span class="p">,</span>
            <span class="nv">$row</span><span class="o">-&gt;</span><span class="n">user_login</span><span class="p">,</span>
            <span class="nv">$row</span><span class="o">-&gt;</span><span class="n">first_name</span><span class="p">,</span>
            <span class="nv">$row</span><span class="o">-&gt;</span><span class="n">last_name</span><span class="p">,</span>
            <span class="nv">$row</span><span class="o">-&gt;</span><span class="n">user_email</span><span class="p">,</span>
            <span class="nv">$row</span><span class="o">-&gt;</span><span class="n">membership_name</span><span class="p">,</span>
            <span class="nv">$row</span><span class="o">-&gt;</span><span class="n">startdate</span><span class="p">,</span>
            <span class="nv">$row</span><span class="o">-&gt;</span><span class="n">enddate</span>
        <span class="p">]);</span>
    <span class="p">}</span>

    <span class="c1">// Close the file</span>
    <span class="nb">fclose</span><span class="p">(</span><span class="nv">$output</span><span class="p">);</span>

    <span class="c1">// Prepare email</span>
    <span class="nv">$to</span> <span class="o">=</span> <span class="s1">''</span><span class="p">;</span> <span class="c1">// Replace with recipient email</span>
    <span class="nv">$subject</span> <span class="o">=</span> <span class="s1">'ISB Monthly Members Report'</span><span class="p">;</span>
    <span class="nv">$message</span> <span class="o">=</span> <span class="s1">'Please find the attached CSV file containing the list of active members of the International Society of Biocuration as of today.'</span><span class="p">;</span>
    <span class="nv">$headers</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span><span class="s1">'Content-Type: text/csv; charset=UTF-8'</span><span class="p">);</span>

    <span class="c1">// Send email with attachment</span>
    <span class="nf">wp_mail</span><span class="p">(</span><span class="nv">$to</span><span class="p">,</span> <span class="nv">$subject</span><span class="p">,</span> <span class="nv">$message</span><span class="p">,</span> <span class="nv">$headers</span><span class="p">,</span> <span class="nv">$tmp_file</span><span class="p">);</span>

    <span class="c1">// Clean up temporary file</span>
    <span class="nb">unlink</span><span class="p">(</span><span class="nv">$tmp_file</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// Schedule the cron job to run monthly</span>
<span class="k">function</span> <span class="n">schedule_monthly_csv_email</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nf">wp_next_scheduled</span><span class="p">(</span><span class="s1">'monthly_pmp_csv_email'</span><span class="p">))</span> <span class="p">{</span>
        <span class="nf">wp_schedule_event</span><span class="p">(</span><span class="nb">time</span><span class="p">(),</span> <span class="s1">'monthly'</span><span class="p">,</span> <span class="s1">'monthly_pmp_csv_email'</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
<span class="nf">add_action</span><span class="p">(</span><span class="s1">'wp'</span><span class="p">,</span> <span class="s1">'schedule_monthly_csv_email'</span><span class="p">);</span>

<span class="c1">// Hook the function to the cron event</span>
<span class="nf">add_action</span><span class="p">(</span><span class="s1">'monthly_pmp_csv_email'</span><span class="p">,</span> <span class="s1">'send_pmp_members_csv_via_email'</span><span class="p">);</span>
</code></pre></div></div>

<p>Hopefully I don’t have to write anymore PHP for a long time :)</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="Wordpress" /><category term="Python" /><category term="PHP" /><summary type="html"><![CDATA[The International Society of Biocuration (ISB) partners with the journal Database to get discounts for its members when they publish there. This means the ISB’s executive committee needs to send a member list to the journal’s editor. Historically, this has been done manually by exporting the list from the membership management plugin in the ISB Wordpress blog once per month and emailing it to th This post is about my journey trying to automate it]]></summary></entry><entry><title type="html">Easier ORCID</title><link href="https://cthoyt.com/2024/06/08/easy-orcid.html" rel="alternate" type="text/html" title="Easier ORCID" /><published>2024-06-08T11:33:00+00:00</published><updated>2024-06-08T11:33:00+00:00</updated><id>https://cthoyt.com/2024/06/08/easy-orcid</id><content type="html" xml:base="https://cthoyt.com/2024/06/08/easy-orcid.html"><![CDATA[<p>The <a href="https://orcid.org/">Open Researcher and Contributor Identifier (ORCID)</a>
database is an invaluable resource that supports the unambiguous identification
of researchers. However, its
<a href="https://doi.org/10.23640/07243.24204912.v1">first party data dump</a> is too
complex, verbose, and unstandardized for many use cases. This post describes
<a href="https://github.com/cthoyt/orcid_downloader">open source software I wrote</a> that
automates downloading, processing, and exporting ORCID into a more usable form.
I put <a href="https://zenodo.org/doi/10.5281/zenodo.10137939">the results</a> on Zenodo
under the CC0 license.</p>

<h2 id="challenges-to-overcome">Challenges to Overcome</h2>

<p>ORCID currently contains on the scale of tens of million records, meaning that
there isn’t a reasonable way to access the data in bulk via its public API. As
an alternative, ORCID dumps its public content once per year on FigShare. The
most recent (2023) dump is available at
<a href="https://doi.org/10.23640/07243.24204912.v1">doi:10.23640/07243.24204912</a>.
Previous versions are deposited with under different DOIs:</p>

<ul>
  <li>2022 ORCID Public Data File (https://doi.org/10.23640/07243.21220892.v4)</li>
  <li>2021 ORCID Public Data File (https://doi.org/10.23640/07243.16750535.v1)</li>
  <li>2020 ORCID Public Data File (https://doi.org/10.23640/07243.13066970)</li>
  <li>…</li>
</ul>

<p>Unfortunately, this arrangement makes it difficult to automatically discover new
versions without writing software that searches FigShare programmatically and
has a heuristic for guessing which might be a newer record. I don’t have a
solution for this yet, but I can imagine one.</p>

<p>Only making a yearly dump means that the derived artifacts can become out of
date quickly. Other large databases like PubChem make monthly and nightly dumps
on their FTP servers which are deleted when no longer relevant. For example,
monthly dumps from more than a year ago can be safely deleted and nightly dumps
only need to exist until they are replaced by the next one. Since ORCID is using
FigShare as an archival system, this would be a disk space-intensive operation.
Using the ORCID API or secondary data distribution via Wikidata could be good
stopgaps for consumers who want the most recent data.</p>

<p>ORCID distributes its data as XML. They also provide Java
<a href="https://github.com/ORCID/orcid-conversion-lib">software</a> to convert it to JSON,
but since 2018 are pretty adamant about not running this software and
distributing the JSON artifact themselves. This is unfortunate, since XML is
hugely verbose both in terms of the way that data gets structure and the markup
itself. Further, the data structure contains a huge amount of provenance
information that isn’t useful for many downstream consumers (both in terms of
when things were updated, by whom, and which API endpoint could be used to
update it further). An
<a href="https://github.com/ORCID/orcid-conversion-lib/blob/master/src/test/resources/test-conversionlib-record-2.1.json">example</a>
from the JSON converter library also illustrates that converting from XML to
JSON accomplish some obvious simplifications that most users would want.</p>

<p>Another tricky thing about consuming the ORCID data is that the summary file
that contains all the records is 32 gigabytes compressed and has a very strange
internal file structure. This means that you either have to uncompress it, which
takes a long time with its tens of millions of files, or iterate through the
file handles inside it. I also haven’t figured out a good way to open a specific
file inside the compressed archive beyond iterating through all the handles. The
file names themselves are also difficult to guess because of the directory
structure used.</p>

<h2 id="solutions">Solutions</h2>

<p>I wrote a Python package,
<a href="https://github.com/cthoyt/orcid_downloader"><code class="language-plaintext highlighter-rouge">orcid_downloader</code></a> that can
automatically download the right file from FigShare, iterate through the
individual compressed XML files for each record, and process them. The package
can be used to iterate over records and process them in your own way like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">orcid_downloader</span>

<span class="k">for</span> <span class="n">record</span> <span class="ow">in</span> <span class="n">orcid_downloader</span><span class="p">.</span><span class="n">iter_records</span><span class="p">():</span>
    <span class="p">...</span>
</code></pre></div></div>

<p>The main goal of this was also to create a simplified version of the ORCID data
dump that is more straightforwards, accessible, and smaller. I would imagine
most people would be interested in just downloading the results instead of
rebuilding them, so the results of this process are posted on Zenodo at
<a href="https://zenodo.org/doi/10.5281/zenodo.10137939">doi:10.5281/zenodo.10137939</a>.
It uses Zenodo’s versioning system to make sure that all different versions
(both from updates to the yearly dump or improvements to the processing
pipeline) are all in the same Zenodo record.</p>

<p>So far, this includes the name, aliases, external identifiers, employers,
education, and publications linked to PubMed. Along the way, I realized that
ORCID did not consistently ground educational institutions to the
<a href="https://ror.org">Research Organization Registry (ROR)</a> like it did for
employers. I also had it double-check all groundings for employers, since these
were incomplete. I also did some minor string processing, such as
standardization of education types (e.g., Bachelor of Science, Master of
Science), standardization of PubMed references, and standardization of aliases
(e.g., pruning off titles like <em>Dr.</em>)</p>

<p>The
<a href="https://zenodo.org/records/11518845/files/records.jsonl.gz?download=1"><code class="language-plaintext highlighter-rouge">records.jsonl.gz</code></a>
file is a JSON Lines file where each row represents a single ORCID record in a
simple, well-defined schema (see
<a href="https://zenodo.org/records/11518845/files/schema.json?download=1"><code class="language-plaintext highlighter-rouge">schema.json</code></a>).
Here are a few rows ( expanded for viewing comfort) as example:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "orcid": "0000-0001-5045-1000",
  "name": "Patricio Sánchez Quinchuela",
  "employments": [
    {
      "name": "Universidad de las Artes",
      "start": 2021,
      "role": "Especialista de Proyectos y docente",
      "xrefs": {
        "ror": "016drwn73"
      }
    },
    {
      "name": "Universidad Regional Amazónica IKIAM",
      "start": 2019,
      "end": 2021,
      "role": "Director",
      "xrefs": {
        "ror": "05xedqd83"
      }
    }
  ],
  "educations": [
    {
      "name": "Universidad Nacional de Educación a Distancia Facultad de Ciencias Políticas y Sociología",
      "start": 2020,
      "role": "Doctorando del Programa de Sociología",
      "xrefs": {
        "ringgold": "223339"
      }
    }
  ]
}
{
  "orcid": "0000-0001-5101-6000",
  "name": "Céline LEPERE"
}
{
  "orcid": "0000-0001-5001-3000",
  "name": "Vincent Nguyen",
  "employments": [
    {
      "name": "Troy High School",
      "start": 2020,
      "role": "Student",
      "xrefs": {
        "ringgold": "289570"
      }
    }
  ]
}
{
  "orcid": "0000-0001-5002-1000",
  "name": "Sameer Abbood",
  "employments": [
    {
      "name": "University of Al-Ameed",
      "role": "Doctor of Philosophy"
    }
  ]
}
</code></pre></div></div>

<p>Many records in ORCID are relatively unhelpful, i.e., ones that only have a name
and no other (public) information. Therefore, I created a high-quality subset
that only contains records with at least one ROR-grounded employer, at least one
ROR-grounded education, or at least one publication indexed in PubMed. The point
of this subset is to remove ORCID records that are generally not possible to
match up to any external information. It is listed in the same Zenodo record as
<a href="https://zenodo.org/records/11518845/files/records_hq.jsonl.gz?download=1"><code class="language-plaintext highlighter-rouge">records_hq.jsonl.gz</code></a>.</p>

<h2 id="external-identifiers">External Identifiers</h2>

<p>An ORCID record has two places that make cross-references to external
nomenclature authorities:</p>

<ol>
  <li>The “Websites &amp; Social Links” section which allows a researcher to give a
link with a name. This is a trove of links to LinkedIn, Google Scholar,
GitHub, and other external identifiers. ORCID itself doesn’t standardize
them, but using a combination of the Bioregistry and custom parsing, many can
be standardized.</li>
  <li>The “Other IDs” section is generated based on applications that connect to
ORCID and send structured links. This includes Scopus, Web of Science
(formerly ResearcherID), Loop, and some others. This also needs quite a bit
of standardization, probably due to a combination of bugs in ORCID, bugs in
external services, legacy data, and other things.</li>
</ol>

<h3 id="reflections">Reflections</h3>

<p>I found several interesting things while parsing these sections:</p>

<ol>
  <li>I discovered several new nomenclature authorities that weren’t already
registered in the
<a href="https://github.com/biopragmatics/bioregistry">Bioregistry</a>. This includes
Loop, Digital Author ID (Dutch), Authenticus (Portuguese), Dialnet (Spanish),
SciProfiles, Ciência (Portuguese), and KAKEN (Japanese). I still have to send
<a href="https://www.youtube.com/watch?v=e-I6rcV2_BE">new prefix requests</a> for these.</li>
  <li>I found several fields that were totally junk or didn’t make sense. For
example, there is sometimes a reference to the ORCID record itself. There are
also references to external IDs that aren’t really IDs, or at least don’t
follow enough of the guidelines from
<a href="https://doi.org/10.1371/journal.pbio.2001414">Identifiers in the 21<sup>st</sup> Century</a>
to be useful.</li>
  <li>Both the “Websites &amp; Social Links” and “Other IDs” needed standardization. In
some places, this was as easy as using the Bioregistry prefix
standardization, but in other places required more custom URL parsing. This
is especially true for Google Scholar, which can appear with a number of
domain names (e.g., https://scholar.google.com or https://scholar.google.es).
The local unique identifier appears here inside the URL parameters, which can
be in any order along with the language tag, so this needs URL parsing
instead of more simple URI prefix handling <em>a la</em> the
<a href="https://github.com/biopragmatics/curies"><code class="language-plaintext highlighter-rouge">curies</code></a> Python Package.</li>
</ol>

<h3 id="summary">Summary</h3>

<p>Here’s a breakdown of the top external cross-references, standardized with the
Bioregistry (where possible). Note that this was prioritized by the most common
cross-references, and is not complete. To capture <em>all</em> would be a lot of work
and require many more corner cases to less common services. I also threw away
links to non-professional social networks like Facebook/Instagram. I also made
the value judgement to throw away links to Twitter since it doesn’t reflect open
and inclusive scientific community values anymore.</p>

<table>
  <thead>
    <tr>
      <th>Resource</th>
      <th style="text-align: right">Count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://bioregistry.io/scopus"><code class="language-plaintext highlighter-rouge">scopus</code></a></td>
      <td style="text-align: right">1,400,735</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/wos.researcher"><code class="language-plaintext highlighter-rouge">wos.researcher</code></a></td>
      <td style="text-align: right">608,543</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">sciprofiles</code></td>
      <td style="text-align: right">259,654</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">loop</code></td>
      <td style="text-align: right">229,224</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/linkedin"><code class="language-plaintext highlighter-rouge">linkedin</code></a></td>
      <td style="text-align: right">191,321</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">researchgate</code></td>
      <td style="text-align: right">125,242</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/google.scholar"><code class="language-plaintext highlighter-rouge">google.scholar</code></a></td>
      <td style="text-align: right">52,131</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/github"><code class="language-plaintext highlighter-rouge">github</code></a></td>
      <td style="text-align: right">13,397</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/gnd"><code class="language-plaintext highlighter-rouge">gnd</code></a></td>
      <td style="text-align: right">7,621</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/isni"><code class="language-plaintext highlighter-rouge">isni</code></a></td>
      <td style="text-align: right">4,105</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">dai</code></td>
      <td style="text-align: right">1,982</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">authenticus</code></td>
      <td style="text-align: right">1,422</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">dialnet</code></td>
      <td style="text-align: right">1,210</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/wikidata"><code class="language-plaintext highlighter-rouge">wikidata</code></a></td>
      <td style="text-align: right">147</td>
    </tr>
  </tbody>
</table>

<p>The results of this process are first available as part of the records file(s).
Second, they are available through a dedicated file
(<a href="https://zenodo.org/records/11474470/files/sssom.tsv.gz?download=1">sssom.tsv.gz</a>)
in the
<a href="https://mapping-commons.github.io/sssom/">Simple Standard for Sharing Ontological Mappings (SSSOM)</a>
that solely focuses on the cross-references. Here’s what the first few lines of
that file look like:</p>

<table>
  <thead>
    <tr>
      <th>subject_id</th>
      <th>subject_label</th>
      <th>predicate_id</th>
      <th>object_id</th>
      <th>mapping_justification</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://bioregistry.io/orcid:0000-0001-5099-6000">orcid:0000-0001-5099-6000</a></td>
      <td>Debashis Bhowmick</td>
      <td>skos:exactMatch</td>
      <td><a href="https://bioregistry.io/scopus:57214299968">scopus:57214299968</a></td>
      <td>semapv:ManualMappingCuration</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/orcid:0000-0001-5009-9000">orcid:0000-0001-5009-9000</a></td>
      <td>Ali Gargouri</td>
      <td>skos:exactMatch</td>
      <td>loop:470724</td>
      <td>semapv:ManualMappingCuration</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/orcid:0000-0001-5009-9000">orcid:0000-0001-5084-9000</a></td>
      <td>Luana Licata</td>
      <td>skos:exactMatch</td>
      <td>loop:1172627</td>
      <td>semapv:ManualMappingCuration</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/orcid:0000-0001-5009-9000">orcid:0000-0001-5084-9000</a></td>
      <td>Luana Licata</td>
      <td>skos:exactMatch</td>
      <td><a href="https://bioregistry.io/scopus:6603618518">scopus:6603618518</a></td>
      <td>semapv:ManualMappingCuration</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/orcid:0000-0001-5124-3000">orcid:0000-0001-5124-3000</a></td>
      <td>Wojciech Nawrocki</td>
      <td>skos:exactMatch</td>
      <td>loop:661557</td>
      <td>semapv:ManualMappingCuration</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/orcid:0000-0001-5075-0000">orcid:0000-0001-5075-0000</a></td>
      <td>Xueyong Pang</td>
      <td>skos:exactMatch</td>
      <td><a href="https://bioregistry.io/wos.researcher:K-6721-2018">wos.researcher:K-6721-2018</a></td>
      <td>semapv:ManualMappingCuration</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/orcid:0000-0001-5103-2000">orcid:0000-0001-5103-2000</a></td>
      <td>Bartlomiej Dec</td>
      <td>skos:exactMatch</td>
      <td><a href="https://bioregistry.io/scopus:57194469902">scopus:57194469902</a></td>
      <td>semapv:ManualMappingCuration</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/orcid:0000-0001-5020-8000">orcid:0000-0001-5020-8000</a></td>
      <td>Sapna Gambhir</td>
      <td>skos:exactMatch</td>
      <td><a href="https://bioregistry.io/scopus:35811915000">scopus:35811915000</a></td>
      <td>semapv:ManualMappingCuration</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/orcid:0000-0001-5074-2000">orcid:0000-0001-5074-2000</a></td>
      <td>Martin Perez-Santos</td>
      <td>skos:exactMatch</td>
      <td><a href="https://bioregistry.io/scopus:56082352000">scopus:56082352000</a></td>
      <td>semapv:ManualMappingCuration</td>
    </tr>
  </tbody>
</table>

<h3 id="what-are-cross-references-useful-for">What are Cross-References Useful For?</h3>

<p>There are many different nomenclature authorities because each have different
goals and data models associated with records. Different communities also value
different nomenclature authorities differently. For example, life scientists are
more often using ORCID when publishing, but including persistent identifiers in
publications is not yet common in computer science papers. Further, computer
scientists more often link to their
<a href="https://bioregistry.io/registry/dblp.author">DBLP</a>, arXiv, OpenReview, or other
computer-science focused pages.</p>

<p>When assembling data and knowledge from more than a single resource, it’s
important to resolve the identifiers used for researchers to a single
identifier - it’s not good if different knowledge is connected to an ORCID and a
DBLP for a single researcher. This can be resolved using a combination of
semantic mappings (i.e., cross-references) and software for the large-scale
automated assembly of mappings such as the
<a href="https://github.com/biopragmatics/semra">Semantic Mapping Reasoning Assembler (SeMRA)</a>.
It allows for specifying a priority list of nomenclature authorities to assemble
coherent knowledge from ORCID and other sources simultaneously.</p>

<p>This is also a much more valuable process when combining other mapping
resources. Wikidata has put a lot of effort into capturing bibliographic
metadata, especially to support the
<a href="https://scholia.toolforge.org/">Scholia project</a>. The following SPARQL query
against Wikidata shows that there are 1,811,573 (about 10% of all ORCID records)
links from Wikidata entries to ORCID as of June 2024.
<a href="https://w.wiki/AHew">Run it yourself</a>.</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="p">(</span><span class="nb">count</span><span class="p">(</span><span class="k">DISTINCT</span><span class="w"> </span><span class="nv">?orcid</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nv">?total</span><span class="p">)</span><span class="w">
</span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">?item</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P496</span><span class="w"> </span><span class="nv">?orcid</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><a href="https://opencheck.is/">OpenCheck</a> tried to create mappings between Twitter,
GitHub, and ORCID using their APIs, but became defunct when Twitter shut off
their public APIs. <a href="https://openalex.org/">OpenAlex</a>,
<a href="https://academic.microsoft.com/">Microsoft Academic Graph</a>,
<a href="https://www.microsoft.com/en-us/research/project/open-academic-graph/">Open Academic Graph</a>,
and other bibliographic knowledge graphs all have to address this problem
internally, as well. Other mapping resources probably exist, please let me know
if you’re aware of other ones!</p>

<h2 id="affiliations">Affiliations</h2>

<p>ORCID breaks up affiliations into several blocks: educations, employments,
invited positions, etc. I’m focusing on educations and employments here. The
processing code could be extended for the others later.</p>

<p>Employments contained ROR references but educations didn’t, and even when
they’re available, they’re incomplete.
<a href="https://github.com/biopragmatics/pyobo">PyOBO</a> has implemented a loader for
ROR. Any resource loaded through PyOBO can also be used for named entity
normalization through an interface to
<a href="https://github.com/gyorilab/gilda">Gilda</a>. This allowed for grounding of a
large number of missing education and employer entries. Here’s the total number
of cross-references made from educations and employments to ROR and other
nomenclature authorities.</p>

<table>
  <thead>
    <tr>
      <th>Resource</th>
      <th style="text-align: right">Count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://bioregistry.io/ror"><code class="language-plaintext highlighter-rouge">ror</code></a></td>
      <td style="text-align: right">8,725,215</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/ringgold"><code class="language-plaintext highlighter-rouge">ringgold</code></a></td>
      <td style="text-align: right">6,614,519</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/grid"><code class="language-plaintext highlighter-rouge">grid</code></a></td>
      <td style="text-align: right">1,404,183</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/funderregistry"><code class="language-plaintext highlighter-rouge">funderregistry</code></a></td>
      <td style="text-align: right">698,384</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">lei</code></td>
      <td style="text-align: right">1,206</td>
    </tr>
  </tbody>
</table>

<h2 id="education-roles">Education Roles</h2>

<p>Both educations and employments also have associated roles. These do not use a
controlled vocabulary, but there are a number of patterns that could be
standardized. This task had to be split between education and employment
entries. I just focused on education entries for now. The “role” field in each
education entry corresponds to the degree.</p>

<p>I started by looking for existing resources (both structured and unstructured)
that have lists of degrees. Here are a few things I found:</p>

<ol>
  <li>https://degree.studentnews.eu lists degrees conferred in the EU/Europe</li>
  <li>https://github.com/vivo-ontologies/academic-degree-ontology is an
incomplete/abandoned effort from 2020 to ontologize degree names</li>
  <li>
    <p>Wikidata has a class for academic degree
https://www.wikidata.org/wiki/Q189533. Its
<code class="language-plaintext highlighter-rouge">SPARQL query service &lt;https://query.wikidata.org&gt;</code>_ can be queried with the
following, though note that the Wikidata class hierarchy is broken in several
places.</p>

    <div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">   </span><span class="k">SELECT</span><span class="w"> </span><span class="nv">?item</span><span class="w"> </span><span class="nv">?itemLabel</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nv">?item</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P279</span><span class="o">*</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q189533</span><span class="w"> </span><span class="p">.</span><span class="w">
      </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">bd</span><span class="o">:</span><span class="ss">serviceParam</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">language</span><span class="w"> </span><span class="s2">"[AUTO_LANGUAGE],en"</span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span></code></pre></div>    </div>
  </li>
</ol>

<p>In the end, these were either incomplete or not organized well enough to
directly use. It also turns out that people often conflate the degree (e.g.,
Master of Science) with the field that it’s in (e.g., Chemistry) or include some
combination (e.g., Master of Science in Chemistry). This meant that a lot of
string processing and heuristics would be required on top of lexical approaches.
Instead, I took the tried-and-true method of listing the strings by frequency
and just curating from the top. The results are in
https://github.com/cthoyt/orcid_downloader/blob/851af81d8aacebf2768bfc591080cbceab2047cc/src/orcid_downloader/standardize.py.
Of course, this is incomplete in many ways, and could be extended to capture
further cases. I also found that there are a huge number of Spanish and
Portuguese entries that I needed help from my international friends to get the
best translations (since the meaning is pretty subtle for many). Further, the
results would be more useful as a proper ontology that could extend and replace
VIVO’s Academic Degree Ontology. I’ll leave this for future work. Here’s a
summary of the most frequent roles that have been standardized so far:</p>

<table>
  <thead>
    <tr>
      <th>Education Role</th>
      <th style="text-align: right">Count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Doctor of Philosophy</td>
      <td style="text-align: right">1,231,845</td>
    </tr>
    <tr>
      <td>Master</td>
      <td style="text-align: right">426,625</td>
    </tr>
    <tr>
      <td>Bachelor</td>
      <td style="text-align: right">402,297</td>
    </tr>
    <tr>
      <td>Master of Science</td>
      <td style="text-align: right">360,052</td>
    </tr>
    <tr>
      <td>Bachelor of Science</td>
      <td style="text-align: right">322,853</td>
    </tr>
    <tr>
      <td>Doctor of Medicine</td>
      <td style="text-align: right">202,884</td>
    </tr>
    <tr>
      <td>Bachelor of Arts</td>
      <td style="text-align: right">108,778</td>
    </tr>
    <tr>
      <td>Master of Arts</td>
      <td style="text-align: right">78,601</td>
    </tr>
    <tr>
      <td>Postdoctoral Researcher</td>
      <td style="text-align: right">37,548</td>
    </tr>
    <tr>
      <td>Bachelor of Medicine, Bachelor of Surgery</td>
      <td style="text-align: right">27,722</td>
    </tr>
    <tr>
      <td>Master of Technology</td>
      <td style="text-align: right">22,473</td>
    </tr>
    <tr>
      <td>Bachelor of Technology</td>
      <td style="text-align: right">21,465</td>
    </tr>
    <tr>
      <td>Bachelor of Engineering</td>
      <td style="text-align: right">21,345</td>
    </tr>
    <tr>
      <td>Diploma</td>
      <td style="text-align: right">20,695</td>
    </tr>
    <tr>
      <td>Master of Business Administration</td>
      <td style="text-align: right">19,660</td>
    </tr>
    <tr>
      <td>Master of Philosophy</td>
      <td style="text-align: right">18,069</td>
    </tr>
    <tr>
      <td>Master of Education</td>
      <td style="text-align: right">18,055</td>
    </tr>
    <tr>
      <td>Master of Public Health</td>
      <td style="text-align: right">17,760</td>
    </tr>
    <tr>
      <td>Bachelor of Education</td>
      <td style="text-align: right">17,376</td>
    </tr>
    <tr>
      <td>Master of Engineering</td>
      <td style="text-align: right">14,260</td>
    </tr>
  </tbody>
</table>

<h2 id="authorship">Authorship</h2>

<p>Authorships are extracted and standardized in the
<a href="https://zenodo.org/records/11518845/files/pubmeds.tsv.gz?download=1"><code class="language-plaintext highlighter-rouge">pubmeds.tsv.gz</code></a>
file, which contains an ORCID column and PubMed column that has been
pre-sanitized to only contain local unique identifiers. This information is also
available through the main records file.</p>

<p>While the field inside the XML data is supposed to contain local unique
identifiers, there was a huge variety of what actually showed up there. This
included local unique identifiers (i.e., <code class="language-plaintext highlighter-rouge">36402838</code>), local unique identifiers
with junk attached (e.g., <code class="language-plaintext highlighter-rouge">36402838/</code>), valid Compact URIs (CURIEs), invalid
CURIEs, URIs, free text that’s totally irrelevant. Overall, there were 3,175,196
(99.85%) that were valid local unique identifiers, 2,832 (0.09%) that were able
to be cleaned up, and 2,080 (0.07%) that were junk and couldn’t be salvaged.
Inside the junk were a few things:</p>

<ul>
  <li>DOIs</li>
  <li>PMC identifiers,</li>
  <li>a few stray strings that contain a combination of pubmed, PMC, and DOIs</li>
  <li>a lot with random text (keywords)</li>
  <li>some with full text citations</li>
</ul>

<p>Later, other identifier types could be added in here too.</p>

<h2 id="lexical-indexing">Lexical Indexing</h2>

<p>One of the original goals of processing ORCID in bulk was to ground and
disambiguate author lists in publications. Therefore, I made two pre-built Gilda
indexes for named entity recognition (NER) and named entity normalization (NEN).
One contains all records, and the second is filtered to high-quality records.
The following Python code snippet can be used for grounding:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">gilda</span> <span class="kn">import</span> <span class="n">Grounder</span>

<span class="n">url</span> <span class="o">=</span> <span class="s">"..."</span>
<span class="n">grounder</span> <span class="o">=</span> <span class="n">Grounder</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="n">results</span> <span class="o">=</span> <span class="n">grounder</span><span class="p">.</span><span class="n">ground</span><span class="p">(</span><span class="s">"Charles Tapley Hoyt"</span><span class="p">)</span>
</code></pre></div></div>

<p>The ORCID downloader also has its own extension that does a smarter job with
caching and some clever name preprocessing</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">orcid_downloader</span>

<span class="n">results</span> <span class="o">=</span> <span class="n">orcid_downloader</span><span class="p">.</span><span class="n">ground_researcher</span><span class="p">(</span><span class="s">"Charles Hoyt"</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="ontology-artifact">Ontology Artifact</h2>

<p>The file
<a href="https://zenodo.org/records/11518845/files/orcid.ttl.gz?download=1"><code class="language-plaintext highlighter-rouge">orcid.ttl.gz</code></a>
is an OWL-ready RDF file that can be opened in
<a href="https://protege.stanford.edu">Protégé</a> or used with the
<a href="https://github.com/INCATools/ontology-development-kit">Ontology Development Kit</a>.
It can also be converted into OWL XML, OWL Functional Notation, or other OWL
formats using <a href="https://robot.obolibrary.org/">ROBOT</a>. This artifact can serve as
a replacement for the ones generated by
<a href="https://github.com/cthoyt/orcidio">https://github.com/cthoyt/orcidio</a>, which
was a smaller-scale way of turning ORCID records for contributors to
<a href="https://obofoundry.org/">OBO Foundry</a> ontologies into a small OWL file. Now,
the export here contains all ORCID records with names.</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@prefix</span><span class="w"> </span><span class="nn">orcid:</span><span class="w"> </span><span class="nl">&lt;https://orcid.org/&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">rdfs:</span><span class="w"> </span><span class="nl">&lt;http://www.w3.org/2000/01/rdf-schema#&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">oboInOwl:</span><span class="w"> </span><span class="nl">&lt;http://www.geneontology.org/formats/oboInOwl#&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">owl:</span><span class="w"> </span><span class="nl">&lt;http://www.w3.org/2002/07/owl#&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">skos:</span><span class="w"> </span><span class="nl">&lt;http://www.w3.org/2004/02/skos/core#&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">human:</span><span class="w"> </span><span class="nl">&lt;http://purl.obolibrary.org/obo/NCBITaxon_9606&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="nn">human:</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">owl:</span><span class="n">Class</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">rdfs:</span><span class="n">label</span><span class="w"> </span><span class="s">"Homo sapiens"</span><span class="p">^^</span><span class="nn">xsd:</span><span class="n">string</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="nn">orcid:</span><span class="mi">0000-0001-5000-5000</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">human:</span><span class="w"> </span><span class="p">;</span><span class="w">
   </span><span class="nn">rdfs:label:</span><span class="w"> </span><span class="s">"Joel Adam Gordon"</span><span class="p">^^</span><span class="nn">xsd:</span><span class="n">string</span><span class="w"> </span><span class="p">;</span><span class="w">
   </span><span class="nn">oboInOwl:</span><span class="n">hasExactSynonym</span><span class="w"> </span><span class="s">"Joel Gordon"</span><span class="p">^^</span><span class="nn">xsd:</span><span class="n">string</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="nn">orcid:</span><span class="mi">0000-0001-5099-6000</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">human:</span><span class="w"> </span><span class="p">;</span><span class="w">
   </span><span class="nn">rdfs:label:</span><span class="w"> </span><span class="s">"Debashis Bhowmick"</span><span class="p">^^</span><span class="nn">xsd:</span><span class="n">string</span><span class="w"> </span><span class="p">;</span><span class="w">
   </span><span class="nn">oboInOwl:</span><span class="n">hasExactSynonym</span><span class="w"> </span><span class="s">"Bhowmick D. S."</span><span class="p">^^</span><span class="nn">xsd:</span><span class="n">string</span><span class="w"> </span><span class="p">;</span><span class="w">
   </span><span class="nn">oboInOwl:</span><span class="n">hasExactSynonym</span><span class="w"> </span><span class="s">"D. S. Bhowmick"</span><span class="p">^^</span><span class="nn">xsd:</span><span class="n">string</span><span class="w"> </span><span class="p">;</span><span class="w">
   </span><span class="nn">oboInOwl:</span><span class="n">hasExactSynonym</span><span class="w"> </span><span class="s">"Debashis S Bhowmick"</span><span class="p">^^</span><span class="nn">xsd:</span><span class="n">string</span><span class="w"> </span><span class="p">;</span><span class="w">
   </span><span class="nn">skos:</span><span class="n">exactMatch</span><span class="w"> </span><span class="s">"scopus:57214299968"</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="nn">orcid:</span><span class="mi">0000-0001-5084-9000</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">human:</span><span class="w"> </span><span class="p">;</span><span class="w">
   </span><span class="nn">rdfs:label:</span><span class="w"> </span><span class="s">"Luana Licata"</span><span class="p">^^</span><span class="nn">xsd:</span><span class="n">string</span><span class="w"> </span><span class="p">;</span><span class="w">
   </span><span class="nn">skos:</span><span class="n">exactMatch</span><span class="w"> </span><span class="s">"loop:1172627"</span><span class="p">^^</span><span class="nn">xsd:</span><span class="n">string</span><span class="w"> </span><span class="p">;</span><span class="w">
   </span><span class="nn">skos:</span><span class="n">exactMatch</span><span class="w"> </span><span class="s">"scopus:6603618518"</span><span class="p">^^</span><span class="nn">xsd:</span><span class="n">string</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>It’s still TBD on the best way to encode the cross-references.</p>

<h2 id="code">Code</h2>

<p>The artifacts described here were all automatically generated with code in
https://github.com/cthoyt/orcid_downloader.</p>

<hr />

<p>Like I mentioned a few times throughout, this is a work in progress. Doing
practical data science is hard work, and there is a lot of room for improvement.
I’m still recovering from burnout, so working at a slow pace only when I felt
inspired also was fine for me. I know there are lots of things I would like to
improve given more motivation, but that will have to wait for now.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="ORCID" /><category term="bibliometrics" /><summary type="html"><![CDATA[The Open Researcher and Contributor Identifier (ORCID) database is an invaluable resource that supports the unambiguous identification of researchers. However, its first party data dump is too complex, verbose, and unstandardized for many use cases. This post describes open source software I wrote that automates downloading, processing, and exporting ORCID into a more usable form. I put the results on Zenodo under the CC0 license.]]></summary></entry><entry><title type="html">Discussions and Follow-ups from Biocuration 2024</title><link href="https://cthoyt.com/2024/03/11/biocuration2024-discussions.html" rel="alternate" type="text/html" title="Discussions and Follow-ups from Biocuration 2024" /><published>2024-03-11T13:52:00+00:00</published><updated>2024-03-11T13:52:00+00:00</updated><id>https://cthoyt.com/2024/03/11/biocuration2024-discussions</id><content type="html" xml:base="https://cthoyt.com/2024/03/11/biocuration2024-discussions.html"><![CDATA[<p>I’ve just returned from the
<a href="https://ibdc.rcb.res.in/biocuration2024/">17<sup>th</sup> Annual
International Biocuration Conference</a> at the Indian Biological Data Centre
(IBDC) in Faridabad, India. I wanted to highlight some of the interesting
conversations I had while I was there, and ideas for follow-up. Most were
centered around the <a href="https://bioregistry.io">Bioregistry</a> and the
<a href="https://github.com/biopragmatics/semra">Semantic Mapping Assembler and Reasoner (SeMRA)</a>,
which I gave an oral presentation on.</p>

<p>I talked to Guy Cochrane and Chuck Cook from the
<a href="https://globalbiodata.org/">Global Biodata Coalition (GBC)</a>. They chaired a
session on sustainability of biocurated resources, with specific focus on the
Global Biodata Coalition’s
<a href="https://globalbiodata.org/what-we-do/global-core-biodata-resources/">Global Biodata Core Resources (GBCR)</a>
initiative. I felt like my talk from last year’s biocuration conference on the
Open Code, Open Data, Open Infrastructure (O3) roadmap
(<a href="https://doi.org/10.31219/osf.io/vuzt3">preprint</a>) would have fit right in
here. I am very keen to have their perspectives, as GBC has first worked on
evaluation of resources and is second working towards funding resources. Since
they have not worked on practical recommendations for supporting sustainability,
I eagerly volunteered to join their work in some capacity to help advise on
this.</p>

<p>GBC also published a workflow for evaluating the landscape of biological
databases
(<a href="https://globalbiodata.org/what-we-do/global-inventory/">press release</a> /
<a href="https://doi.org/10.1371/journal.pone.0294812">publication</a> /
<a href="https://github.com/globalbiodata/inventory_2022/">code</a>). When possible, this
workflow aligned on
<a href="https://bioregistry.io/metaregistry/fairsharing">FAIRsharing</a>, but given that
it is a limited resource and only has partial mappings to relevant related
resources like <a href="https://bioregistry.io/metaregistry/re3data">re3code</a>,
<a href="https://bioregistry.io/metaregistry/bartoc">BARTOC</a>, etc. I suggested using the
Bioregistry as a mapping hub to enrich the output of this workflow, which will
definitely be run again on a periodic basis.</p>

<p>Lynn Schriml presented recent updates on the Disease Ontology, which prompted a
relevant question from Harpreet Singh - Chief Data Officer at the Indian Council
of Medical Research (ICMR) who himself works with clinical data and has wondered
how to best annotate - using <a href="https://bioregistry.io/mesh">MeSH</a>,
<a href="https://bioregistry.io/snomed">SNOMED</a>, <a href="https://bioregistry.io/icd11">ICD</a>, or
other disease resources. I had an interesting discussion with him following the
talk which gave big motivation to the talk I was about to give on the
<a href="https://bit.ly/biocuration2024-cth">large scale assembly and reasoning over semantic mappings</a>.
I was very excited, since I love to add (last minute) shout-outs into my
conference talks that motivate parts of the work based on questions or
discussions from earlier parts of the conference.</p>

<p>There were a series of talks that motivated further discussions about mappings.
One of the most interesting was the talk from Shivani Sharma, a curator at the
<a href="https://ibdc.rcb.res.in/">Indian Biological Data Centre (IBDC)</a> and one of the
local organizers. She works on the
<a href="https://ibdc.rcb.res.in/imda/">Indian Metabolome Data Archive</a>. Many of the
lines of work at the IBDC have practical applications towards agriculture and
integrate medium- and large-scale experimental work, biocuration, and downstream
analysis. Often, these applications are oriented towards improving crop yields
and avoiding disease. Shivani showed a slide where they considered a large
number of metabolomics nomenclature resources to use for annotating their data.
However, they were not familiar with methods for incorporating multiple
nomenclature resources, meaning that their curators were running into issues
where their chosen metabolomics database did not cover chemicals they needed to
annotate. This often lead to them having to create their own <em>ad hoc</em>
annotations, which also create issues for data integration. I am looking
forwards to catching up with them again, incorporating new metabolomics resource
into <a href="https://github.com/biopragmatics/pyobo">PyOBO</a>, ingesting mappings into
<a href="https://github.com/biopragmatics/semra">SeMRA</a>, and filling in the gaps using
Biomappings to support their curators.</p>

<p>Scott V. Nguyen from the
<a href="https://bioregistry.io/atcc">American Type Culture Collection (ATCC)</a> also
approached me about this work, since he’s currently trying to curate mappings
between cell lines in their resource and other public resources. It was lucky
that one of the examples from my talk was specifically about the cell lince
scenario, which I hope he can ingest and reduce his curation workload. Rachel
Lyne also presented on <a href="https://bioregistry.io/cosmic.cell">COSMIC</a>, a cancer
cell line resource that also creates its own accession numbers and could benefit
from this work, but I didn’t get a chance to talk about it with her yet.</p>

<p>I also met Yasunori Yamamoto, who works on <a href="https://togoid.dbcls.jp/">TogoID</a>, a
secondary database of semantic mappings that covers select domains within
biomedicine. We discussed how they could make use of the
<a href="https://academic.oup.com/database/article/doi/10.1093/database/baac035/6591806">Simple Standard for Sharing Ontology Mappings (SSSOM)</a>
to ingest more mappings from different resources, especially from Biomappings or
potentially from the outputs of SeMRA (which I presented on).</p>

<p>Matt Jeffreys presented on the annotations database in
<a href="https://europepmc.org/">European PubMed Central</a> which allows for tagging
articles, sentences, or tokens in articles with annotations. They already showed
how this applies to named entity recognition (NER) and MeSH term annotations,
but we discussed how SeMRA and comprehensive semantic mapping databases could
help unify other annotations of overlapping vocabularies, e.g., if someone put
Disease Ontology (DO) NER annotations, which overlap with MeSH terms in the
disease (C) and psychiatric disorders (F) branches.</p>

<p>I discussed with Raja Mazumdar and Jeet Vora from George Washington University
who both work on <a href="https://bioregistry.io/glygen">GlyGen</a> and are plugged into
the NIH’s
<a href="https://commonfund.nih.gov/dataecosystem">Common Fund Data Ecosystem (CFDE)</a>
about how they can continue to use the Bioregistry to standardize the
annotations in their resources. Jeet has got in touch earlier this year and
helped update the records in the Bioregistry related to GlyGen. Raja’s talk also
motivated two new prefix additions to the Bioregistry for Biocompute Objects and
for OncoMX data objects. Further, Raja is very interested in improving his data
using the Bioregistry, since it already uses a Python script to validate its
JSON and TSV components, it will be easy to incorporate the Bioregistry Python
package’s validation functions.</p>

<p>Earlier this winter, I presented to the American National Institutes of Health
(NIH) BISTI group about different avenues through which they could use the
Bioregistry to create more value for the NIH and its grantees. One of those
discussions was about improving GenBank’s
<a href="https://www.ncbi.nlm.nih.gov/genbank/collab/db_xref/">internal database catalog</a>.
By chance, I talked with Ilene Karsch Mizrachi, a program head at the NIH about
this. She was attending the conference and made big contributions to the
discussions about the Indian relation to the International Nucleotide Sequence
Database Collaboration (INSDC). However, it turns out she was the one who
made/contributed to this GenBank table, many years ago. We will try and follow
up by enriching this table with information from the Bioregistry.</p>

<p>At last year’s biocuration conference, Chris Hunter presented on
<a href="http://gigadb.org/">GigaDB</a>, and we had some initial discussions about using
the Bioregistry (or other related parts of the Biopragmatics Stack) to make
standardized annotations on data sets deposited in their database, such as cell
line annotations. We picked back up that conversation, and it seems that the
GigaDB developers are working with PHP - since we got CZI funding to make the
Bioregistry available in other languages, making a wrapper from Rust to PHP
(within the <a href="https://github.com/biopragmatics/curies.rs">curies.rs</a> framework).</p>

<p>There was an entire session on the final day of the conference on structural
bioinformatics, which included several presentations from the American and
<a href="https://www.ebi.ac.uk/pdbe">European</a> loci of the Protein Databank (PDB). The
first discussion was with Marcus Bage, who is currently trying to annotate
protein modifications. We discussed the implications of the vast number of
resources that partially cover this domain in different senses, including
<a href="https://bioregistry.io/go">GO</a>, <a href="https://bioregistry.io/mod">MOD</a>,
<a href="https://bioregistry.io/sbo">SBO</a>, <a href="https://bioregistry.io/mop">MOP / PSI-MI</a>,
and
<a href="https://bioregistry.io/registry/uniprot.ptm">UniProt’s internal vocabulary</a>. A
long time ago, I mapped these together in
<a href="https://github.com/pybel/pybel/blob/ed66f013a77f9cbc513892b0dad1025b8f68bb46/src/pybel/language.py#L346-L582">PyBEL</a>,
but this was only a partial solution, too!</p>

<p>The second discussion was with Brinda Vallat about the upcoming change for PDB
accession numbers. It turns out that the 4 character code is estimated to fill
up in 2029, so it’s time for PDB to make a change. Unfortunately, their solution
is to switch to local unique identifiers that look like <code class="language-plaintext highlighter-rouge">pdb_000002GC4</code>, which
is problematic for two main reasons. First, it’s not backwards compatible with
existing IDs. Second, it introduces a banana (i.e., a redundant copy of the
name/acronym of the database in the local unique identifier). The reasoning
behind adding in the banana was to make it easier to find references in papers.
I can understand this, since we don’t yet have general solutions for referencing
concepts across different publishers (though, we solved this in Manubot by
integrating the Bioregistry). However, this increases confusion for consumers. I
suggested they simply extend the existing IDs to be able to have more than 4
characters, and suggest people reference their entities with CURIEs like
<code class="language-plaintext highlighter-rouge">PDB:2GC4</code> within papers, which solves both issues simultaneously. Similarly, I
talked to Ibrahim Roshan Kunnakkattu about creating more careful identifier
recommendations for the PDB’s
<a href="https://bioregistry.io/registry/pdb-ccd">Chemical Component Dictionary</a> as well
as using some of the automated mapping tools I presented for filling out
references to ChEBI, ChEMBL, PubChem, and more.</p>

<p>I also had the unique pleasure to spend time in person with Tiago Lubiana, who
is highly aligned on many of my interests in data standardization, semantic web,
and open science. He has been a helpful contributor in the Bioregistry,
Wikidata, and the OBO Foundry. Writing up some of the things we discussed would
take a whole blog post, so instead, here’s a nice picture we got together.</p>

<p><img src="/img/charlie_tiago_march_2024.jpg" alt="Charlie Hoyt and Tiago Lubiana" /></p>

<hr />

<p>Overall, like every Biocuration conference, I was very happy to find people
interested in my work, and more importantly, interested in the idea of improving
their own data standardization! I also had lots of other interesting discussions
that don’t require any follow-up. I am also planning on writing a post that
gives a more high-level summary of the different parts of the conference itself,
not just focusing on my work.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="International Society of Biocuration" /><category term="biocuration" /><summary type="html"><![CDATA[I’ve just returned from the 17th Annual International Biocuration Conference at the Indian Biological Data Centre (IBDC) in Faridabad, India. I wanted to highlight some of the interesting conversations I had while I was there, and ideas for follow-up. Most were centered around the Bioregistry and the Semantic Mapping Assembler and Reasoner (SeMRA), which I gave an oral presentation on.]]></summary></entry><entry><title type="html">Semantic Pydantic</title><link href="https://cthoyt.com/2024/01/10/semantic-pydantic.html" rel="alternate" type="text/html" title="Semantic Pydantic" /><published>2024-01-10T13:38:00+00:00</published><updated>2024-01-10T13:38:00+00:00</updated><id>https://cthoyt.com/2024/01/10/semantic-pydantic</id><content type="html" xml:base="https://cthoyt.com/2024/01/10/semantic-pydantic.html"><![CDATA[<p>Using <a href="https://docs.pydantic.dev">Pydantic</a> for encoding data models and
<a href="https://fastapi.tiangolo.com">FastAPI</a> for implementing APIs on top of them has
become a staple for many Python programmers. When this intersects with the
semantic web, linked open data, and the natural sciences, we are still lacking a
bridge to annotate our data models and APIs to make them more FAIR (findable,
accessible, interoperable, and reusable). In this post, we build an extension to
Pydantic and FastAPI to annotate data models’ fields and API endpoints’ query,
path, and other parameters using the <a href="https://bioregistry.io">Bioregistry</a>, a
comprehensive catalog of metadata about semantic spaces from the semantic web
and the natural sciences.</p>

<p>As a demonstration, we will build a data model and API that serves information
about scholars.</p>

<h2 id="first-steps-with-pydantic">First Steps with Pydantic</h2>

<p>We’ll use <a href="https://orcid.org/">Open Researcher and Contributor (ORCID)</a>
identifiers as primary keys, include the researcher’s name, and start with a
single cross-reference, e.g., to the author’s <a href="https://dblp.org/">DBLP</a>
identifier. We’ll encode this data model using
<a href="https://docs.pydantic.dev/latest/">Pydantic</a> in the Python programming language
as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span><span class="p">,</span> <span class="n">Field</span>


<span class="k">class</span> <span class="nc">ScholarV1</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="s">"""A model representing a researcher, who might have several IDs on different services."""</span>

    <span class="n">orcid</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(...)</span>
    <span class="n">name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(...)</span>
    <span class="n">dblp</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="bp">None</span><span class="p">)</span>


<span class="k">print</span><span class="p">(</span><span class="n">ScholarV1</span><span class="p">.</span><span class="n">schema_json</span><span class="p">(</span><span class="n">indent</span><span class="o">=</span><span class="mi">2</span><span class="p">))</span>
</code></pre></div></div>

<details>
<summary>JSON Schema - Version 1</summary>

```json
{
  "title": "ScholarV1",
  "description": "A model representing a researcher, who might have several IDs on different services.",
  "type": "object",
  "properties": {
    "orcid": {
      "title": "Orcid",
      "type": "string"
    },
    "name": {
      "title": "Name",
      "type": "string"
    },
    "dblp": {
      "title": "Dblp",
      "type": "string"
    }
  },
  "required": ["orcid", "name"]
}
```

</details>

<p>There are several places for improvement here:</p>

<ol>
  <li>Correct capitalization of the titles (<code class="language-plaintext highlighter-rouge">ORCID</code> instead of <code class="language-plaintext highlighter-rouge">Orcid</code> and <code class="language-plaintext highlighter-rouge">DBLP</code>
instead of <code class="language-plaintext highlighter-rouge">Dblp</code>)</li>
  <li>Add useful descriptions of what each field is</li>
  <li>Have regular expression patterns to validate input</li>
  <li>Give an example</li>
</ol>

<p>All of these are possible to annotate into Pydantic’s <code class="language-plaintext highlighter-rouge">Field</code> object, but it
requires lots of effort and takes lots of space. Even worse, this might have to
be partially duplicated if multiple models share the same fields. In the example
below, I annotated ORCID but will skip the others for brevity.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span><span class="p">,</span> <span class="n">Field</span>


<span class="k">class</span> <span class="nc">ScholarV2</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="s">"""A model representing a researcher, who might have several IDs on different services."""</span>

    <span class="n">orcid</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
        <span class="p">...,</span>
        <span class="n">title</span><span class="o">=</span><span class="s">"ORCID"</span><span class="p">,</span>
        <span class="n">description</span><span class="o">=</span><span class="s">"A stable, public identifier for a researcher from https://orcid.com"</span><span class="p">,</span>
        <span class="n">pattern</span><span class="o">=</span><span class="s">"^\d{4}-\d{4}-\d{4}-\d{3}(\d|X)$"</span><span class="p">,</span>
        <span class="n">example</span><span class="o">=</span><span class="s">"0000-0003-4423-4370"</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="n">name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(...)</span>
    <span class="n">dblp</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="bp">None</span><span class="p">)</span>


<span class="k">print</span><span class="p">(</span><span class="n">ScholarV2</span><span class="p">.</span><span class="n">schema_json</span><span class="p">(</span><span class="n">indent</span><span class="o">=</span><span class="mi">2</span><span class="p">))</span>
</code></pre></div></div>

<details>
<summary>JSON Schema - Version 2</summary>

```json
{
  "title": "ScholarV2",
  "description": "A model representing a researcher, who might have several IDs on different services.",
  "type": "object",
  "properties": {
    "orcid": {
      "title": "ORCID",
      "description": "A stable, public identifier for a researcher from https://orcid.com",
      "pattern": "^\\d{4}-\\d{4}-\\d{4}-\\d{3}(\\d|X)$",
      "example": "0000-0003-4423-4370",
      "type": "string"
    },
    "name": {
      "title": "Name",
      "type": "string"
    },
    "dblp": {
      "title": "Dblp",
      "type": "string"
    }
  },
  "required": ["orcid", "name"]
}
```

</details>

<p>However, this was a lot of work. It would be nice if there were some database of
all the semantic spaces in the semantic web and natural sciences that contained
the name, description, regular expression pattern, and examples. Then, we could
draw from this database to automatically populate our fields.</p>

<h2 id="adding-some-bioregistry-magic">Adding Some Bioregistry Magic</h2>

<p>The good news is that such a database exists - it’s called the
<a href="https://bioregistry.io">Bioregistry</a>. Each semantic space (e.g., ORCID, DBLP)
gets a <em>prefix</em> which is usually an acronym for the name of the resource that
serves as the primary key for the semantic space. These prefixes are also useful
in making references to entities in the semantic space more FAIR (findable,
accessible, interoperable, reusable) using the
<a href="https://cthoyt.com/2021/09/14/curies.html">compact URI (CURIE) syntax</a>, though
this isn’t the goal of this demo.</p>

<p>I’ve mocked some Python code that bridges Pydantic and the Bioregistry in this
repository (https://github.com/cthoyt/semantic-pydantic). I’m calling it
<strong>Semantic Pydantic</strong> because it lets us annotate our data models with external
metadata (and because it rhymes).</p>

<p>Here’s the same model as before, but now using a <code class="language-plaintext highlighter-rouge">SemanticField</code> that extends
Pydantic’s <code class="language-plaintext highlighter-rouge">Field</code>. It has a special keyword <code class="language-plaintext highlighter-rouge">prefix</code> that lets you give a
Bioregistry prefix, then it is smart enough to fill out all the fields on its
own. I also took the liberty of adding several more semantic spaces that
identify scholars like
<a href="https://bioregistry.io/wos.researcher">Web of Science (<code class="language-plaintext highlighter-rouge">wos</code>)</a>,
<a href="https://bioregistry.io/scopus">Scopus</a>, and even
<a href="https://bioregistry.io/github">GitHub</a>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span><span class="p">,</span> <span class="n">Field</span>

<span class="kn">from</span> <span class="nn">semantic_pydantic</span> <span class="kn">import</span> <span class="n">SemanticField</span>


<span class="k">class</span> <span class="nc">ScholarV3</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="s">"""A model representing a researcher, who might have several IDs on different services."""</span>

    <span class="n">orcid</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">SemanticField</span><span class="p">(...,</span> <span class="n">prefix</span><span class="o">=</span><span class="s">"orcid"</span><span class="p">)</span>
    <span class="n">name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(...,</span> <span class="n">example</span><span class="o">=</span><span class="s">"Charles Tapley Hoyt"</span><span class="p">)</span>

    <span class="n">wos</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="n">SemanticField</span><span class="p">(</span><span class="n">default</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">prefix</span><span class="o">=</span><span class="s">"wos.researcher"</span><span class="p">)</span>
    <span class="n">dblp</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="n">SemanticField</span><span class="p">(</span><span class="n">default</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">prefix</span><span class="o">=</span><span class="s">"dblp.author"</span><span class="p">)</span>
    <span class="n">github</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="n">SemanticField</span><span class="p">(</span><span class="n">default</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">prefix</span><span class="o">=</span><span class="s">"github"</span><span class="p">)</span>
    <span class="n">scopus</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="n">SemanticField</span><span class="p">(</span><span class="n">default</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">prefix</span><span class="o">=</span><span class="s">"scopus"</span><span class="p">)</span>
    <span class="n">semion</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="n">SemanticField</span><span class="p">(</span><span class="n">default</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">prefix</span><span class="o">=</span><span class="s">"semion"</span><span class="p">)</span>
    <span class="n">publons</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="n">SemanticField</span><span class="p">(</span><span class="n">default</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">prefix</span><span class="o">=</span><span class="s">"publons.researcher"</span><span class="p">)</span>
    <span class="n">authorea</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="n">SemanticField</span><span class="p">(</span><span class="n">default</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">prefix</span><span class="o">=</span><span class="s">"authorea.author"</span><span class="p">)</span>


<span class="k">print</span><span class="p">(</span><span class="n">ScholarV3</span><span class="p">.</span><span class="n">schema_json</span><span class="p">(</span><span class="n">indent</span><span class="o">=</span><span class="mi">2</span><span class="p">))</span>
</code></pre></div></div>

<p>Finally, we can see a very detailed JSON schema, which includes everything from
before plus additional context from the Bioregistry, including the prefix itself
as well as mappings from the Bioregistry prefix to external registries like
<a href="https://bioregistry.io/metaregistry/bartoc">BARTOC</a>,
<a href="https://bioregistry.io/metaregistry/fairsharing">FAIRsharing</a>, and others.
Together, these make the data model more FAIR and support interoperability,
since now it is possible to directly match the fields annotated with Bioregistry
prefixes in this model to fields annotated with the same prefix in other models,
even external to the project.</p>

<details>
<summary>JSON Schema - Version 3</summary>

```json
{
  "title": "ScholarV3",
  "description": "A model representing a researcher, who might have several IDs on different services.",
  "type": "object",
  "properties": {
    "orcid": {
      "title": "Open Researcher and Contributor",
      "description": "<p>This field corresponds to a local unique identifier from <i>Open Researcher and Contributor</i>&lt;/a&gt;.\n</p><h4>Provenance</h4><p>The semantics of this field are derived from the\n&lt;a href=\"https://bioregistry.io/orcid\"&gt;<code>orcid</code>&lt;/a&gt; entry in\nthe &lt;a href=\"https://bioregistry.io\"&gt;Bioregistry&lt;/a&gt;: a registry of semantic web and linked \nopen data compact URI (CURIE) prefixes and URI prefixes.\n</p><h4>Description of Semantic Space</h4>ORCID (Open Researcher and Contributor ID) is an open, non-profit, community-based effort to create and maintain a registry of unique identifiers for individual researchers. ORCID records hold non-sensitive information such as name, email, organization name, and research activities.",
      "pattern": "^\\d{4}-\\d{4}-\\d{4}-\\d{3}(\\d|X)$",
      "example": "0000-0003-4423-4370",
      "json_schema_extra": {
        "bioregistry": {
          "prefix": "orcid",
          "mappings": {
            "bartoc": "2021",
            "biocontext": "ORCID",
            "biolink": "ORCID",
            "fairsharing": "FAIRsharing.nx58jg",
            "go": "orcid",
            "miriam": "orcid",
            "n2t": "orcid",
            "wikidata": "P496"
          }
        }
      },
      "type": "string"
    },
    "name": {
      "title": "Name",
      "example": "Charles Tapley Hoyt",
      "type": "string"
    },
    ...
  }
}
```

Note that the extra semantic fields have been truncated for brevity. Each of
them also has lots of detailed information.

</details>

<h2 id="web-application">Web Application</h2>

<p>Let’s take the next step to a web application using FastAPI. The goal of this
web application will be to look up the information for a scholar in Wikidata
based on their ORCID. You don’t really have to understand how the query works
other than that it takes in an ORCID string and gives back an instance of the
Scholar model we’ve been working on above.</p>

<p>The app uses annotations for the query parameters, path parameters, and other
inputs to routes using extensions of Pydantic <code class="language-plaintext highlighter-rouge">Fields</code>. So similar to before, we
can extend their custom fields to be semantic in <strong>Semantic Pydantic</strong>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">fastapi</span> <span class="kn">import</span> <span class="n">FastAPI</span>
<span class="kn">from</span> <span class="nn">semantic_pydantic</span> <span class="kn">import</span> <span class="n">SemanticPath</span>

<span class="n">app</span> <span class="o">=</span> <span class="n">FastAPI</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">"Semantic Pydantic Demo"</span><span class="p">)</span>
<span class="n">Scholar</span> <span class="o">=</span> <span class="p">...</span>  <span class="c1"># defined before
</span>

<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"/api/orcid/{orcid}"</span><span class="p">,</span> <span class="n">response_model</span><span class="o">=</span><span class="n">Scholar</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get_scholar_from_orcid</span><span class="p">(</span><span class="n">orcid</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">SemanticPath</span><span class="p">(</span><span class="n">prefix</span><span class="o">=</span><span class="s">"orcid"</span><span class="p">)):</span>
    <span class="s">"""Get xrefs for a researcher in Wikidata, given ORCID identifier."""</span>
    <span class="p">...</span>  <span class="c1"># full implementation in https://github.com/cthoyt/semantic-pydantic
</span>    <span class="k">return</span> <span class="n">Scholar</span><span class="p">(...)</span>
</code></pre></div></div>

<p>The real power is how this translates to the API, and more importantly, the
automatically generated API documentation. First, the <code class="language-plaintext highlighter-rouge">SemanticPath</code> object
which we used in place of a normal <code class="language-plaintext highlighter-rouge">fastapi.Path</code> also knows it is for ORCID
identifiers. Second, the response model points to the Scholar class from before
which already knows about its semantics. Below, we see this in a screenshot of
the OpenAPI (formerly known as Swagger) user interface automatically generated
by FastAPI.</p>

<p><img src="/img/semantic-pydantic-api.png" alt="A screenshot of the Swagger UI, enriched by the semantic annotations described above." /></p>

<p>There are two big things to note here:</p>

<ol>
  <li>All the detailed information about ORCID makes it into the parameter, also
giving an example for users to get started</li>
  <li>All the detailed information from the response model (Scholar) gets shown as
a real example.</li>
</ol>

<p>Now, we have an API that is also annotated with detailed semantics. If you take
a look at the OpenAPI JSON file, it has similar references to Bioregistry
prefixes for the routes themselves, and directly reuses the JSON schema for the
response model.</p>

<h2 id="next-steps">Next Steps</h2>

<p>So far, this is a proof-of-concept that lives in an <em>ad hoc</em> repository. It’s
not clear yet if this code is just a neat demo, whether it should live inside
the Bioregistry Python package, I haven’t decided yet if this should go inside
the <a href="https://github.com/biopragmatics/bioregistry/">Bioregistry Python package</a>,
or if it should be in a stand-alone package that might be extensible even
further. There are a few other things to think about in the meantime:</p>

<h3 id="infrastructure-for-fair-models-and-apis">Infrastructure for FAIR Models and APIs</h3>

<p>The first version of this idea just throws the Bioregistry data into the JSON
schema. It would be interesting to develop this infrastructure further, such as
keeping a catalog of all APIs that consume or produce data models containing
semantic fields. A few places this would be great:</p>

<ol>
  <li>The
<a href="https://github.com/Steinbeck-Lab/cheminformatics-microservice/issues">Cheminformatics Microservice</a>
contains tons of references to
<a href="https://bioregistry.io/registry/inchikey">InChi Keys (<code class="language-plaintext highlighter-rouge">inchikey</code>)</a></li>
  <li>The Bioregistry could dog-food its own API</li>
  <li>The <a href="https://discovery.indra.bio/apidocs">INDRA Discovery API</a> could be
refactored to use semantic inputs and outputs</li>
</ol>

<p>There are also so many more examples, please let me know some services you think
would benefit in the comments on my blog post. Looking forward, it’s also a
question on how to automatically discover such semantic APIs (e.g., by cleverly
searching GitHub) or if it would have to be a manually curated catalog.</p>

<h3 id="resolving-urls">Resolving URLs</h3>

<p>A key feature of the Bioregistry is that it provides a way to take a local
unique identifier for an entity in a given semantic space and make a URL that
points to a web page describing the entity. For example, if you have an ORCID
identifier, you can make a URL for the ORCID page following the format
<code class="language-plaintext highlighter-rouge">https://orcid.org/&lt;put ID here&gt;</code>. It would be very cool to extend Semantic
Pydantic to add some properties that auto-generate URLs, like in the following:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span><span class="p">,</span> <span class="n">Field</span>

<span class="kn">from</span> <span class="nn">semantic_pydantic</span> <span class="kn">import</span> <span class="n">SemanticField</span>


<span class="k">class</span> <span class="nc">Scholar</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">orcid</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">SemanticField</span><span class="p">(...,</span> <span class="n">prefix</span><span class="o">=</span><span class="s">"orcid"</span><span class="p">)</span>
    <span class="n">name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(...)</span>


<span class="n">charlie</span> <span class="o">=</span> <span class="n">Scholar</span><span class="p">(</span><span class="n">orcid</span><span class="o">=</span><span class="s">"0000-0003-4423-4370"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"Charles Tapley Hoyt"</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">charlie</span><span class="p">.</span><span class="n">orcid_url</span> <span class="o">==</span> <span class="s">'https://orcid.org/0000-0003-4423-4370'</span>
</code></pre></div></div>

<h3 id="run-the-demo">Run the Demo</h3>

<p>The demo can be run by cloning the repository, installing its requirements, and
running the self-contained <code class="language-plaintext highlighter-rouge">app.py</code>.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/cthoyt/semantic-pydantic
<span class="nb">cd </span>semantic-pydantic
python <span class="nt">-m</span> pip <span class="nb">install</span> <span class="nt">-r</span> requirements.txt
python app.py
</code></pre></div></div>

<hr />

<p>This is my first science post of 2024! I’m very happy that the Bioregistry is
currently supported by the Chan Zuckerberg Initiative (CZI) under award
2023-329850.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="Bioregistry" /><category term="semantic web" /><category term="Pydantic" /><category term="FastAPI" /><category term="Python" /><summary type="html"><![CDATA[Using Pydantic for encoding data models and FastAPI for implementing APIs on top of them has become a staple for many Python programmers. When this intersects with the semantic web, linked open data, and the natural sciences, we are still lacking a bridge to annotate our data models and APIs to make them more FAIR (findable, accessible, interoperable, and reusable). In this post, we build an extension to Pydantic and FastAPI to annotate data models’ fields and API endpoints’ query, path, and other parameters using the Bioregistry, a comprehensive catalog of metadata about semantic spaces from the semantic web and the natural sciences.]]></summary></entry><entry><title type="html">Books I Read in 2023</title><link href="https://cthoyt.com/2024/01/01/books-in-2023.html" rel="alternate" type="text/html" title="Books I Read in 2023" /><published>2024-01-01T21:22:00+00:00</published><updated>2024-01-01T21:22:00+00:00</updated><id>https://cthoyt.com/2024/01/01/books-in-2023</id><content type="html" xml:base="https://cthoyt.com/2024/01/01/books-in-2023.html"><![CDATA[<p>I finally got back into reading! Over winter break 2022, I started the
Stormlight Archive then followed up in 2023 by reading the entirety of Brandon
Sanderson’s <em>Cosmere</em>, as well as a some other fantasy, science fiction, and
literary fiction. Here’s the list.</p>

<ol>
  <li>Oathbringer (Stormlight Archive #3) by Brandon Sanderson</li>
  <li>Rhythm of War (Stormlight Archive #4) by Brandon Sanderson</li>
  <li>The Final Empire (Mistborn #1) by Brandon Sanderson</li>
  <li>The Well of Ascension (Mistborn #2) by Brandon Sanderson</li>
  <li>The Hero of Ages (Mistborn #3) by Brandon Sanderson</li>
  <li>The Alloy of Law (Mistborn #4) by Brandon Sanderson</li>
  <li>Shadows of Self (Mistborn #5) by Brandon Sanderson</li>
  <li>The Bands of Mourning (Mistborn #6) by Brandon Sanderson</li>
  <li>Warbreaker by Brandon Sanderson</li>
  <li>Elantris by Brandon Sanderson</li>
  <li>The Shadow of the Wind (The Cemetery of Forgotten Books #1) by Carlos Ruiz
Zafón</li>
  <li>The Angel’s Game (The Cemetery of Forgotten Books #2) by Carlos Ruiz Zafón</li>
  <li>The Emperor’s Soul by Brandon Sanderson</li>
  <li>The Hope of Elantris by Brandon Sanderson</li>
  <li>The Eleventh Metal by Brandon Sanderson</li>
  <li>Mistborn: Secret History by Brandon Sanderson</li>
  <li>Shadows for Silence in the Forests of Hell by Brandon Sanderson</li>
  <li>Sixth of Dusk by Brandon Sanderson</li>
  <li>White Sands by Brandon Sanderson</li>
  <li>The Lost Metal (Mistborn #7) by Brandon Sanderson</li>
  <li>Edgedancer (Stormlight Archive #2.5) by Brandon Sanderson</li>
  <li>What It Means When a Man Falls from the Sky by Lesley Nneka Arimah</li>
  <li>Tress of the Emerald Sea by Brandon Sanderson</li>
  <li>The Prisoner of Heaven (The Cemetery of Forgotten Books #3) by Carlos Ruiz
Zafón</li>
  <li>The Name of the Wind (The Kingkiller Chronicle #1) by Patrick Rothfuss</li>
  <li>The Wise Man’s Fear (The Kingkiller Chronicle #2) by Patrick Rothfuss</li>
  <li>The Labyrinth of Spirits (The Cemetery of Forgotten Books #4) by Carlos Ruiz
Zafón</li>
  <li>Yumi and the Nightmare Painter by Brandon Sanderson</li>
  <li>Hyperion (The Hyperion Cantos #1) by Dan Simmons</li>
  <li>The Fall of Hyperion (The Hyperion Cantos #2) by Dan Simmons</li>
  <li>Dawnshard (Stormlight Archive #3.5) by Brandon Sanderson</li>
  <li>The Sunlit Man by Brandon Sanderson</li>
  <li>All Systems Red (Murderbot Dairies #1) by Martha Wells</li>
  <li>Endymion (The Hyperion Cantos #3) by Dan Simmons</li>
  <li>The Rise of Endymion (The Hyperion Cantos #4) by Dan Simmons</li>
  <li>A Memory Called Empire (Teixcalaan #1) by Arkady Martine</li>
  <li>A Desolation Called Peace (Teixcalaan #2) by Arkady Martine</li>
  <li>Crime and Punishment by Fyodor Dostoevsky</li>
  <li>The Strange Case of Dr. Jekyll and Mr. Hyde by Robert Louis Stevenson</li>
  <li>Anna Karenina by Leo Tolstoy</li>
  <li>Red Rising (Red Rising Saga #1) by Pierce Brown</li>
  <li>Golden Son (Red Rising Saga #2) by Pierce Brown</li>
  <li>This is How You Lose the Time War by Amal El-Mohtar and Max Gladstone</li>
</ol>

<p>Cosmere moments I really enjoyed (spoilers):</p>

<ol>
  <li>Every single oath (finished or not), particularly Elhokar and Eshonai</li>
  <li>I guessed the twist in Tress of the Emerald Sea</li>
  <li>Kelsier’s not dead!</li>
  <li>Dalinar Kholin’s entire character arc</li>
  <li>Wayne, Pattern, and Nightblood being hilarious</li>
  <li>Michael Kramer and Kate Reading’s voices</li>
  <li>All the tidbits in Sunlit Man… I can’t wait for Stormlight 5 to fill in
some of the gaps.</li>
</ol>

<p>Other non-Cosmere highlights (spoilers):</p>

<ol>
  <li>Narrator reveal at the end of The Labyrinth of Spirits</li>
  <li>Anna Karenina’s inner monologue as she commits suicide was devastating</li>
  <li>Rothfuss’s prose is like honey</li>
  <li>The messiah arc in the second Hyperion duology is really nice. It’s a much
better version of the awful “love connects the universe” cliché at the end of
Interstellar that actually creates plot- and character-driven relevance.</li>
</ol>

<p>Disappointments:</p>

<ol>
  <li>Knowing the Doors of Stone might never come out but reading Kingkiller
Chronicle #1 and #2 anyway</li>
  <li>I didn’t like that All Systems Red had the entire plot get resolved
off-screen. My sisters were really hoping I’d like this book, and I was only
neutral. I’ll still give the next one a try.</li>
  <li>I missed the Year of Sanderson Kickstarter (and the hilarity of the
announcement video). I hadn’t really started until after.</li>
  <li>The Russian classics were pretty slow, but I guess that’s how it goes.</li>
</ol>

<p>My goal in 2024 is to read more books from different genres, especially ones
I’ve never touched before.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="books" /><summary type="html"><![CDATA[I finally got back into reading! Over winter break 2022, I started the Stormlight Archive then followed up in 2023 by reading the entirety of Brandon Sanderson’s Cosmere, as well as a some other fantasy, science fiction, and literary fiction. Here’s the list.]]></summary></entry><entry><title type="html">Unlocking UMLS</title><link href="https://cthoyt.com/2023/09/01/umls.html" rel="alternate" type="text/html" title="Unlocking UMLS" /><published>2023-09-01T08:00:00+00:00</published><updated>2023-09-01T08:00:00+00:00</updated><id>https://cthoyt.com/2023/09/01/umls</id><content type="html" xml:base="https://cthoyt.com/2023/09/01/umls.html"><![CDATA[<p>The
<a href="https://www.nlm.nih.gov/research/umls">Unified Medical Language System (UMLS)</a>
is a widely used biomedical and clinical vocabulary maintained by the United
States National Library of Medicine. However, it is notoriously difficult to
access and work with due to licensing restrictions and its complex download
system. In the same vein as my previous posts about
<a href="/2020/12/14/taming-drugbank.html">DrugBank</a> and
<a href="/2021/08/05/taming-chembl-sql.html">ChEMBL</a>, this post describes
<a href="https://github.com/cthoyt/umls_downloader">open source software</a> I’ve developed
for downloading and working with this data. It also works for
<a href="https://www.nlm.nih.gov/research/umls/rxnorm/docs/rxnormfiles.html">RxNorm</a>,
<a href="https://lhncbc.nlm.nih.gov/ii/tools/SemRep_SemMedDB_SKR/SemMedDB_download.html">SemMedDB</a>,
<a href="https://www.nlm.nih.gov/healthit/snomedct/international.html">SNOMED-CT</a>, and
any other data accessible through the
<a href="https://documentation.uts.nlm.nih.gov/automating-downloads.html">UMLS Terminology Services (UTS) ticket granting system</a>.</p>

<p>The first big issue with the UMLS is its licensing. Here’s an excerpt from the
<a href="https://www.nlm.nih.gov/databases/umls.html">How to License and Access the Unified Medical Language System® (UMLS®) Data</a>
page accessed on August 28<sup>th</sup>, 2023:</p>

<blockquote>
  <ol>
    <li>Please sign up for a new UMLS Terminology Services (UTS) account with your
preferred identity provider at the UTS</li>
  </ol>
</blockquote>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> homepage.
</code></pre></div></div>

<blockquote>
  <ol>
    <li>Complete and submit the license request form. NLM will send the license
approval e-mail within 5 business days</li>
  </ol>
</blockquote>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> after reviewing your authenticated license request.
</code></pre></div></div>

<blockquote>
  <ol>
    <li>You will sign in using identity provider credentials to download files or
access web interfaces that require UTS</li>
  </ol>
</blockquote>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> authentication such as the UTS, VSAC, SNOMED CT, or RxNorm.
</code></pre></div></div>

<p>These are a few big hurdles:</p>

<ol>
  <li>We typically expect scientific data to be available for download without
login. Specifically, most data can be downloaded by following a link that
points directly to a file. For example, ChEMBL v33 can be downloaded as a
gzipped SQLite file from
<a href="https://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/latest/chembl_33_sqlite.tar.gz">https://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/latest/chembl_33_sqlite.tar.gz</a>.
Rather than providing a data download link, UMLS, has a complicated API
called the UMLS Terminology Services (UTS) ticket granting system that needs
to be asked for a specific file, polled for a unique access key, then
downloaded via an ephemeral (i.e., disappearing) URL that only works once.</li>
  <li>We typically expect scientific data to be licensed under a standard, widely
used license such as those from
<a href="https://creativecommons.org">Creative Commons</a>. Using well-understood
licenses reduces the cognitive and legal burden of consumers when deciding if
and how they can reuse, modify, or redistribute data. UMLS uses a
non-standard <a href="https://www.nlm.nih.gov/databases/umls.html">terms of service</a>
that makes it more difficult to understand how the data can be stored,
modified, or redistributed.</li>
  <li>Further, we hope that data is permissively licensed such that it can be
re-used, modified, and re-distributed. The Creative Commons
<a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a> and
<a href="https://creativecommons.org/publicdomain/zero/1.0/">CC0</a> licenses are golden
standards of permissibility. UMLS does not have a permissive license, meaning
(from my best interpretation) that you can not redistribute UMLS and you can
(probably) not redistribute data derived from UMLS. As an aside, Creative
Commons also has license containing clauses to be explicit about restrictions
such as the share-alike (SA), non-commercial (NC), and non-distribution (ND).
While these clauses aren’t ideal for scientific data, it would at least be
nice for UMLS to use a Creative Commons license with the appropriate
combination of these clauses (I guess all three) to make it more explicit
about its restrictions.</li>
  <li>The most bizarre facet of UMLS is that they require you to fill out a user
survey each year to keep access.</li>
</ol>

<p>I want to 1) convert UMLS into an <a href="https://www.w3.org/OWL/">OWL</a> ontology and 2)
extract and encode its semantic mappings to external vocabularies like the
<a href="https://bioregistry.iomesh">Medical Subject Headings (MeSH)</a> with
<a href="https://github.com/mapping-commons/sssom">Simple Standard for Sharing Ontology Mappings (SSSOM)</a>.
Given all of these hurdles, it’s probably the case that I am not allowed to
redistribute these artifacts.</p>

<p>All together, I consider this a big bummer. The United States National Library
of Medicine (NLM) maintains several highly influential resources, but I have
found in many instances that they lack a community perspective. Regardless, even
as an expat, I pay American taxes, and it makes me upset that the government
funds the development and maintenance of resources that I can’t easily use.</p>

<h2 id="how-to-break-free">How To Break Free</h2>

<p><img src="https://media.tenor.com/lfjcIHfOhqoAAAAC/freddie-mercury-queen.gif" alt="A clip of Queen's &quot;I Want to Break Free&quot; music video" /></p>

<p>Despite all of this rigamarole, there’s a process to subvert these issues by
automating the interaction with the
<a href="https://uts.nlm.nih.gov/uts">UMLS Terminology Services (UTS)</a> and therefore
enabling automated download of UMLS and the following (non-exhaustive) list of
resources:</p>

<ul>
  <li><a href="https://www.nlm.nih.gov/research/umls/rxnorm/docs/rxnormfiles.html">RxNorm</a></li>
  <li><a href="https://lhncbc.nlm.nih.gov/ii/tools/SemRep_SemMedDB_SKR/SemMedDB_download.html">SemMedDB</a></li>
  <li><a href="https://www.nlm.nih.gov/healthit/snomedct/international.html">SNOMED-CT</a></li>
  <li>potentially more in the future</li>
</ul>

<p>This has been implemented in the open source
<a href="https://github.com/cthoyt/umls_downloader"><code class="language-plaintext highlighter-rouge">umls_downloader</code></a> Python package.
It can be installed with the following one-liner in your shell:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pip <span class="nb">install </span>umls_downloader
</code></pre></div></div>

<p>Below, I’ll walk you through using it. Throughout, keep in mind that full
documentation for the <code class="language-plaintext highlighter-rouge">umls_downloader</code> is available at
<a href="https://umls-downloader.readthedocs.io">umls-downloader.readthedocs.io</a>, which
describes the other functionality and other data that can be downloaded.</p>

<h3 id="usage">Usage</h3>

<p>UMLS has three different distributions that are described
<a href="https://www.nlm.nih.gov/research/umls/licensedcontent/umlsknowledgesources.html">here</a>.
The following Python code downloads the most simple and straightforward file,
<code class="language-plaintext highlighter-rouge">MRCONSO.RRF</code> as a zip archive:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">umls_downloader</span> <span class="kn">import</span> <span class="n">download_umls</span>

<span class="n">path</span> <span class="o">=</span> <span class="n">download_umls</span><span class="p">(</span><span class="n">version</span><span class="o">=</span><span class="s">"2023AA"</span><span class="p">,</span> <span class="n">api_key</span><span class="o">=</span><span class="s">"&lt;your API key&gt;"</span><span class="p">)</span>
</code></pre></div></div>

<p>This code is smart and does not need to download the file more than once. It
uses <a href="https://github.com/cthoyt/pystow"><code class="language-plaintext highlighter-rouge">pystow</code></a> to choose a stable path
<code class="language-plaintext highlighter-rouge">~/.data/bio/umls</code> relative to the current user’s home directory. Inside this
directory, it also uses the version of the data to create a subdirectory.
Finally, this function returns the path to the data, such that no file paths
ever need to be hard-coded.</p>

<blockquote>
  <p><strong>Warning</strong> This still requires an API key, which requires creating an
account, agreeing to UMLS’s terms and conditions, etc. This can be done here:
<a href="https://uts.nlm.nih.gov/uts/edit-profile">https://uts.nlm.nih.gov/uts/edit-profile</a>.</p>
</blockquote>

<h3 id="automating-configuration-of-uts-credentials">Automating Configuration of UTS Credentials</h3>

<p>There are two ways to automatically set the API key, so you don’t have to worry
about getting it and passing it around in your python code:</p>

<ol>
  <li>Set <code class="language-plaintext highlighter-rouge">UMLS_API_KEY</code> in the environment. This can be done in your interactive
session or in the configuration for your shell such as in a <code class="language-plaintext highlighter-rouge">.bashrc</code> file
for the Bourne Again Shell (bash).</li>
  <li>Create <code class="language-plaintext highlighter-rouge">~/.config/umls.ini</code> and set in the <code class="language-plaintext highlighter-rouge">[umls]</code> section a <code class="language-plaintext highlighter-rouge">api_key</code> key.
Mine looks like:
    <div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[umls]</span>
<span class="py">api_key</span><span class="p">=</span><span class="s">1234567890abcdefghijklmno</span>
</code></pre></div>    </div>
  </li>
</ol>

<p>Now you can omit the <code class="language-plaintext highlighter-rouge">api_key</code> keyword like in the following:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">umls_downloader</span> <span class="kn">import</span> <span class="n">download_umls</span>

<span class="c1"># Same path as before
</span><span class="n">path</span> <span class="o">=</span> <span class="n">download_umls</span><span class="p">(</span><span class="n">version</span><span class="o">=</span><span class="s">"2023AA"</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="download-the-latest-version">Download the Latest Version</h3>

<p>First, you’ll have to install
<a href="https://github.com/cthoyt/bioversions"><code class="language-plaintext highlighter-rouge">bioversions</code></a> with
<code class="language-plaintext highlighter-rouge">pip install bioversions</code>, whose job it is to look up the latest version of many
databases. Then, you can modify the previous code slightly by omitting the
<code class="language-plaintext highlighter-rouge">version</code> keyword argument:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">umls_downloader</span> <span class="kn">import</span> <span class="n">download_umls</span>

<span class="c1"># Same path as before (when run on September 1st, 2023)
</span><span class="n">path</span> <span class="o">=</span> <span class="n">download_umls</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="download-and-open-the-file">Download and open the file</h3>

<p>The UMLS file is zipped, so it’s usually accompanied by the following
boilerplate code:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">zipfile</span>
<span class="kn">from</span> <span class="nn">umls_downloader</span> <span class="kn">import</span> <span class="n">download_umls</span>

<span class="n">path</span> <span class="o">=</span> <span class="n">download_umls</span><span class="p">()</span>
<span class="k">with</span> <span class="n">zipfile</span><span class="p">.</span><span class="n">ZipFile</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="k">as</span> <span class="n">zip_file</span><span class="p">:</span>
    <span class="k">with</span> <span class="n">zip_file</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="s">"MRCONSO.RRF"</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="s">"r"</span><span class="p">)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
        <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="nb">file</span><span class="p">:</span>
            <span class="p">...</span>
</code></pre></div></div>

<p>This exact code is wrapped with the <code class="language-plaintext highlighter-rouge">umls_downloader.open_umls()</code> using Python’s
context manager, so it can more simply be written as:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">umls_downloader</span> <span class="kn">import</span> <span class="n">open_umls</span>

<span class="k">with</span> <span class="n">open_umls</span><span class="p">()</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
    <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="nb">file</span><span class="p">:</span>
        <span class="p">...</span>
</code></pre></div></div>

<blockquote>
  <p><strong>Note</strong> The <code class="language-plaintext highlighter-rouge">version</code> and <code class="language-plaintext highlighter-rouge">api_key</code> arguments work the same for
<code class="language-plaintext highlighter-rouge">umls_downloader.open_umls()</code> as in <code class="language-plaintext highlighter-rouge">umls_downloader.download_umls()</code></p>
</blockquote>

<p>At this point, it’s up to you to decide how you want to consume the
<code class="language-plaintext highlighter-rouge">MRCONSO.RRF</code> file. Below, I give a demo on how parsed this file in PyOBO in
order to convert UMLS to an OWL ontology.</p>

<h2 id="why-not-an-api">Why not an API?</h2>

<p>The UMLS provides an <a href="https://documentation.uts.nlm.nih.gov/rest/home.html">API</a>
for access to tiny bits of data at a time. There are even two recent (last 5
years) packages <a href="https://pypi.org/project/umls-api"><code class="language-plaintext highlighter-rouge">umls-api</code></a>
<a href="https://pypi.org/project/connect-umls"><code class="language-plaintext highlighter-rouge">connect-umls</code></a> that provide a wrapper
around them. However, API access is generally rate limited, difficult to use in
bulk, and slow. For working with UMLS (or any other database, for that matter)
in bulk, it’s necessary to download full database dumps.</p>

<h2 id="umls-conversions">UMLS Conversions</h2>

<p>Building on top of the automated download of UMLS, I implemented a
fit-for-purpose processor with the
<a href="https://github.com/pyobo/pyobo">PyOBO framework</a> that converts UMLS into an
ontology (encoded either as OWL, OBO, or OBO Graph JSON) which can therefore be
used to generate semantic mappings in the SSSOM format. The code that implements
this can be found
<a href="https://github.com/pyobo/pyobo/commit/be67eadf77afa270cf224cad71d5b1905ae78b53">here</a>.
After installing PyOBO with <code class="language-plaintext highlighter-rouge">pip install pyobo</code>, you can automatically download
and convert UMLS first into an ontology encoded in the
<a href="https://owlcollab.github.io/oboformat/doc/GO.format.obo-1_4.html">OBO flat file format</a>,
then convert to OWL with the following code. Note: you’ll need
<a href="https://robot.obolibrary.org/"><code class="language-plaintext highlighter-rouge">robot</code></a> for the second step:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pyobo</span>

<span class="n">umls</span> <span class="o">=</span> <span class="n">pyobo</span><span class="p">.</span><span class="n">get_ontology</span><span class="p">(</span><span class="s">"umls"</span><span class="p">)</span>

<span class="c1"># Write simple OBO Format
</span><span class="n">umls</span><span class="p">.</span><span class="n">write_obo</span><span class="p">(</span><span class="s">"umls.obo"</span><span class="p">)</span>

<span class="c1"># Convert to OWL
</span><span class="kn">from</span> <span class="nn">pyobo.utils.misc</span> <span class="kn">import</span> <span class="n">obo_to_owl</span>

<span class="n">obo_to_owl</span><span class="p">(</span><span class="s">"umls.obo"</span><span class="p">,</span> <span class="s">"umls.owl"</span><span class="p">)</span>
</code></pre></div></div>

<p>In an ideal world, the results of such a conversion could be included as a part
of the <a href="https://github.com/biopragmatics/obo-db-ingest">OBO Database Ingestion</a>,
which converts database resources available through PyOBO into ontology
artifacts, archives them on GitHub and Zenodo, and gives them PURLs all on a
weekly basis to make sure the most up-to-date version is available as well as
all previous named versions. Instead, we live in a world with pineapple pizza
and restrictive licenses.</p>

<p>One of the nice qualities of UMLS is that it is a semantic mapping hub. It
provides mostly complete mappings between many vocabularies including
<a href="https://bioregistry.io/mesh">MeSH</a>, <a href="https://bioregistry.io/ncit">NCIT</a>,
<a href="https://bioregistry.io/snomedct">SNOMED-CT</a>, <a href="https://bioregistry.io/hpo">HPO</a>,
<a href="https://bioregistry.io/loinc">LOINC</a>, and more. However, there are a few
caveats to consider:</p>

<ol>
  <li>UMLS mappings aren’t all 1-to-1. For example, MeSH mappings typically include
many UMLS terms (narrower) pointing to the same MeSH term (broader). For
other vocabularies, such as NCBITaxon, UMLS mappings are more reliably
1-to-1. Thanks to Tiago Lubiana for pointing this out.</li>
  <li>Mapping provenance is not available, so the <code class="language-plaintext highlighter-rouge">mapping_justification</code> field in
SSSOM is uniformly filled with <code class="language-plaintext highlighter-rouge">sempav:UnspecifiedMatching</code>.</li>
  <li>Similarly, UMLS does not apply precise semantic predicates for each mapping.
This means that they are output in PyOBO and as SSSOM with
<code class="language-plaintext highlighter-rouge">oboInOwl:hasDbXref</code> instead of more detailed types such as
<code class="language-plaintext highlighter-rouge">skos:exactMatch</code>, <code class="language-plaintext highlighter-rouge">skos:narrowMatch</code>, and <code class="language-plaintext highlighter-rouge">skos:broaderMatch</code>. Tools like
<a href="https://github.com/INCATools/boomer">Boomer</a> can be used to address this (in
part). The
<a href="https://github.com/biopragmatics/semra">Semantic Mapping Reasoning Assembler (SeMRA)</a>
can also be configured with prior knowledge about UMLS mapping assumptions
when aggregating and reasoning over semantic mappings at scale.</li>
</ol>

<p>With that in mind, anything that can be loaded as an ontology in PyOBO can also
be exported with SSSOM, which I show below. For UMLS, this looks like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pyobo</span>

<span class="n">df</span> <span class="o">=</span> <span class="n">pyobo</span><span class="p">.</span><span class="n">get_sssom_df</span><span class="p">(</span><span class="s">"umls"</span><span class="p">,</span> <span class="n">names</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">df</span><span class="p">.</span><span class="n">to_csv</span><span class="p">(</span><span class="s">"umls.sssom.tsv"</span><span class="p">,</span> <span class="n">sep</span><span class="o">=</span><span class="s">"</span><span class="se">\t</span><span class="s">"</span><span class="p">,</span> <span class="n">index</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
</code></pre></div></div>

<blockquote>
  <p><strong>Note</strong> You can set <code class="language-plaintext highlighter-rouge">names=True</code> to have PyOBO look up the names for all
entities, but this is a bit of a rabbit hole since it requires getting and
processing many external resources.</p>
</blockquote>

<hr />

<p>There’s much more to say about UMLS and SSSOM, but this is a good place to pause
and publish this post, since getting UMLS as SSSOM is a task a lot of people
have asked me for help with lately. I might also come back and explain more
about how I use the other resources from UMLS’s UTS.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="UMLS" /><category term="reproducibility" /><summary type="html"><![CDATA[The Unified Medical Language System (UMLS) is a widely used biomedical and clinical vocabulary maintained by the United States National Library of Medicine. However, it is notoriously difficult to access and work with due to licensing restrictions and its complex download system. In the same vein as my previous posts about DrugBank and ChEMBL, this post describes open source software I’ve developed for downloading and working with this data. It also works for RxNorm, SemMedDB, SNOMED-CT, and any other data accessible through the UMLS Terminology Services (UTS) ticket granting system.]]></summary></entry><entry><title type="html">Reproducibility Pilot in the Journal of Cheminformatics</title><link href="https://cthoyt.com/2023/08/27/jcheminf-reproducibility-pilot.html" rel="alternate" type="text/html" title="Reproducibility Pilot in the Journal of Cheminformatics" /><published>2023-08-27T08:00:00+00:00</published><updated>2023-08-27T08:00:00+00:00</updated><id>https://cthoyt.com/2023/08/27/jcheminf-reproducibility-pilot</id><content type="html" xml:base="https://cthoyt.com/2023/08/27/jcheminf-reproducibility-pilot.html"><![CDATA[<p>I’ve been working on improving reproducibility in the field of cheminformatics
for some time now. For example, I’ve written posts about making data from
<a href="/2020/12/14/taming-drugbank.html">DrugBank</a> and
<a href="/2021/08/05/taming-chembl-sql.html">ChEMBL</a> more actionable. Over the
last year, I’ve been preparing a concept with the editors of the
<a href="https://jcheminf.biomedcentral.com">Journal of Cheminformatics</a> on how to
include an assessment of reproducibility to reviews of manuscripts submitted to
the journal. This has resulted in an editorial
<a href="https://doi.org/10.1186/s13321-023-00730-y">Improving reproducibility and reusability in the Journal of Cheminformatics</a>
as well as a
<a href="https://www.biomedcentral.com/collections/irrijc">call for papers</a>. In this
post, I want to summarize the first generation review criteria we developed,
give an example of it applied in practice</p>

<h2 id="the-seven-first-generation-criteria">The Seven (First Generation) Criteria</h2>

<p>There are many potential directions for reproducibility. Given the fact that
typical computational scientists are not trained as software engineers, we
decided on seven very simple criteria that can be easily reviewed and easily
addressed:</p>

<ol>
  <li>Does the repository contain a LICENSE file in its root?</li>
  <li>Does the repository contain a README file in its root?</li>
  <li>Does the repository contain an associated public issue tracker?</li>
  <li>Has the repository been externally archived on <a href="https://zenodo.org/">Zenodo</a>,
<a href="https://figshare.com/">FigShare</a>, or equivalent that is referenced in the
README?</li>
  <li>Does the README contain installation documentation?</li>
  <li>Is the code from the repository installable in a straight-forward manner?</li>
  <li>Does the code conform to an external linter (e.g.,
<a href="https://github.com/psf/black"><code class="language-plaintext highlighter-rouge">black</code></a> for Python)?</li>
</ol>

<p>These correspond to important details that are complementary to other
considerations of reproducibility, but often overlooked. Throughout the pilot,
the editors and reviewers will try to support authors in addressing each of
these points during revision. I imagine that there will be future iterations of
these criteria as the community begins to expect these as standard practice. For
example, we can narrow criteria 1 to specifically say that the software should
be licensed with an OSI-approved license and not accept science made with
non-open licenses. We could further narrow point 7 to have additional community
style requirements (e.g., passes parts of <code class="language-plaintext highlighter-rouge">flake8</code>, as you know I love from my
post <a href="/2020/04/25/how-to-code-with-me-flake8.html">on flake8 hell</a>). We
could also include additional guidelines that e.g. say that the results
presented in the paper should be reproducible with a single command from the
command line, e.g., a shell script. The rabbit hole could go very deep, so
again, it’s worth saying that these are very non-controversial criteria for the
first generation.</p>

<p>That being said, many repositories don’t follow these! Since these criteria are
so simple, I’m interested in automating their assessment and further applying it
to the entire Journal of Cheminformatics backlog. I’ll describe this more in a
future post.</p>

<p>Without further ado, the text below is what I sent verbatim in the review for
<a href="https://doi.org/10.21203/rs.3.rs-3242419/v1">Drug-Protein Interaction Prediction via Multi-View Variational Autoencoder and Cascade Deep Forests</a>,
which is pre-printed on Research Square and has associated code
<a href="https://github.com/Macau-LYXia/MVAE-DFDTnet">here</a>. I have tried my best to
include actionable links and information with each piece. I would like to also
automate sending separate GitHub issues for each of these points as a more
concrete to-do list for authors, then also send an “epic” issue that lists all
of them together. With the magic of the GitHub API, this is possible.</p>

<h2 id="my-first-reproducibility-review-for-the-reproducibility-pilot">My First Reproducibility Review for the Reproducibility Pilot</h2>

<p>Below, I apply the seven point reproducibility review prescribed by
<a href="https://doi.org/10.1186/s13321-023-00730-y">Improving reproducibility and reusability in the Journal of Cheminformatics</a>
to the default branch of repository https://github.com/Macau-LYXia/MVAE-DFDTnet
(commit
<a href="https://github.com/Macau-LYXia/MVAE-DFDTnet/commit/c0858c8cdf61d62f945fbd74d0d60f2531394287">c0858c8</a>),
accessed on August 27<sup>th</sup>, 2023.</p>

<ol>
  <li><strong>Does the repository contain a LICENSE file in its root?</strong><br /> No. The
GitHub license picker can be used to facilitate adding one by following this
link:
https://github.com/Macau-LYXia/MVAE-DFDTnet/community/license/new?branch=main.
Ideal software licenses for open source software include the MIT License, BSD
family of licenses, and other licenses approved by the
<a href="https://opensource.org/licenses">Open Source Initiative</a>. A simple,
informative guide for picking a license can be found at
https://choosealicense.com.</li>
  <li>Does the repository contain a README file in its root?<br /> No. A minimal
viable README file contains:
    <ul>
      <li>A short, one line description of the project</li>
      <li>Information on how to download, install, and run the code locally</li>
      <li>Brief documentation describing the single most important use case for the
repository. For scientific code, this is ideally a one-liner in Python
code, a shell script, or a command line interface (CLI) that can be used to
reproduce the results of the analysis presented in a corresponding
manuscript, use the tool presented in the manuscript, etc.</li>
      <li>Link to an archive on an external system like Zenodo, FigShare, or an
equivalent.</li>
      <li>Citation information, e.g., for a pre-print then later for a peer reviewed
manuscript</li>
    </ul>

    <p>GitHub can be used to create a README file with
https://github.com/Macau-LYXia/MVAE-DFDTnet/new/main?filename=README.md.
Repositories typically use the Markdown format, which is explained
<a href="https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax">here</a>.</p>
  </li>
  <li>Does the repository contain an associated public issue tracker?<br /> Yes. It
is available at https://github.com/Macau-LYXia/MVAE-DFDTnet/issues.</li>
  <li>Has the repository been externally archived on Zenodo, FigShare, or an
equivalent that is referenced in the README?<br /> No, there is no README.
This is also not mentioned in the manuscript. See
https://docs.github.com/en/repositories/archiving-a-github-repository/referencing-and-citing-content
for a tutorial on how to do this.</li>
  <li>Does the README contain installation documentation?<br /> No, there is no
README. This is also not mentioned in the manuscript.</li>
  <li>Is the code in the repository installable in a straight-forward manner?<br />
No, the code is not laid out in a typical structure, e.g., as described in
https://blog.ionelmc.ro/2014/05/25/python-packaging. Further, there is no
setup configuration that encodes the dependencies or facilitates reuse of the
code.</li>
  <li>Does the code in the repository conform to an external linter (e.g., <code class="language-plaintext highlighter-rouge">black</code>
for Python)?<br /> No. The Python code has not been linted, e.g., using
<a href="https://github.com/psf/black"><code class="language-plaintext highlighter-rouge">black</code></a>. Similarly, the Matlab code has not
been linted, e.g. using
<a href="https://de.mathworks.com/help/matlab/ref/checkcode.html"><code class="language-plaintext highlighter-rouge">checkcode</code></a>.</li>
</ol>

<p>Scientific integrity depends on enabling others to understand the methodology
(written as computer code) and reproduce the results generated from it. This
reproducibility review reflects steps towards this goal that may be new for some
researchers, but will ultimately raise standards across our community and lead
to better science. Because the work presented in this article only yet address
one of the seven points of the reproducibility review, I recommend rejecting the
article and inviting later resubmission following addressing the points.</p>

<p>For posterity, this review has also been included on
https://github.com/Macau-LYXia/MVAE-DFDTnet/issues/1.</p>

<h2 id="the-future-is-looking-good">The Future is Looking Good</h2>

<p>The example above isn’t so great - it’s possible that these authors have never
considered most of these points about reproducibility before. The reality is
that many computational scientists are not trained in this since their mentors
were not primarily trained as computational scientists themselves. Combine with
the perverse incentive structure in academia, it’s understandable how this can
be left out from some publications. I experienced something similar in my
doctoral studies, and had to bootstrap my own philosophy on reproducibility as
well as the practical skills to achieve it. I also understand not everyone is in
the position where they have the flexibility/freedom/initiative to do this.</p>

<p>That all being said, we are now entering an era where progressive and newly
minted PIs actually have training as computational scientists. The next paper in
my queue for a reproducibility review is for
https://github.com/Steinbeck-Lab/cheminformatics-python-microservice, which will
pass the 7 criteria with flying colors. I’m looking forward to the future when
we expect more excellent science on the regular. See you there!</p>

<hr />

<p>I’m not sure how people will view the way I talk about reviews - I am quite open
with posting reviews on GitHub and also openly discussing the fact that I’ve
reviewed something. Ideally, I don’t accept reviews for papers that don’t have
pre-prints, since I personally think the review process should be open. I hope
it’s the case that I haven’t been rude or unfair. If that’s the case, someone
can help me change the way I write about these topics.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="cheminformatics" /><category term="reproducibility" /><summary type="html"><![CDATA[I’ve been working on improving reproducibility in the field of cheminformatics for some time now. For example, I’ve written posts about making data from DrugBank and ChEMBL more actionable. Over the last year, I’ve been preparing a concept with the editors of the Journal of Cheminformatics on how to include an assessment of reproducibility to reviews of manuscripts submitted to the journal. This has resulted in an editorial Improving reproducibility and reusability in the Journal of Cheminformatics as well as a call for papers. In this post, I want to summarize the first generation review criteria we developed, give an example of it applied in practice]]></summary></entry><entry><title type="html">Querying Journals and Publishers in Wikidata</title><link href="https://cthoyt.com/2023/06/22/wikidata-publishers.html" rel="alternate" type="text/html" title="Querying Journals and Publishers in Wikidata" /><published>2023-06-22T17:00:23+00:00</published><updated>2023-06-22T17:00:23+00:00</updated><id>https://cthoyt.com/2023/06/22/wikidata-publishers</id><content type="html" xml:base="https://cthoyt.com/2023/06/22/wikidata-publishers.html"><![CDATA[<p>Today’s short post is about three SPARQL queries I wrote to get bibliometric
information about journals and publishers out of Wikidata.</p>

<p>Each of the following queries can be readily copy-pasted into the
<a href="https://query.wikidata.org/">Wikidata Query Service</a> and run in the browser.</p>

<h2 id="journals">Journals</h2>

<p>The following SPARQL query gets information about journals:</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="nv">?journal</span><span class="w"> </span><span class="nv">?journalLabel</span><span class="w"> </span><span class="p">(</span><span class="nb">GROUP_CONCAT</span><span class="p">(</span><span class="nv">?issn</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nv">?issns</span><span class="p">)</span><span class="w">
</span><span class="k">WHERE</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nv">?journal</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P31</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q5633421</span><span class="w"> </span><span class="p">;</span><span class="w">
           </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P236</span><span class="w"> </span><span class="nv">?issn</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">bd</span><span class="o">:</span><span class="ss">serviceParam</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">language</span><span class="w"> </span><span class="s2">"[AUTO_LANGUAGE],en"</span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c1"># Helps get the label in your language, if not, then en language</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="nv">?journal</span><span class="w"> </span><span class="nv">?journalLabel</span><span class="w">
</span></code></pre></div></div>

<p>Follow <a href="https://w.wiki/6ryy">this link</a> to populate the Wikidata Query Service
with this query. Note that this query takes a while to run and may time out
since there are on the scale of 100K journals.</p>

<p>Journals might have multiple
<a href="https://bioregistry.io/registry/issn">International Standard Serial Numbers (ISSNs)</a>
because a different one is assigned to the print and electronic versions of the
journal, among other things.</p>

<p>Get the ISSN-L (the normalized/preferred) ISSN for each:</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="nv">?journal</span><span class="w"> </span><span class="nv">?journalLabel</span><span class="w"> </span><span class="nv">?issn</span><span class="w">
</span><span class="k">WHERE</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nv">?journal</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P31</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q5633421</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">OPTIONAL</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">?journal</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P7363</span><span class="w"> </span><span class="nv">?issnl</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">bd</span><span class="o">:</span><span class="ss">serviceParam</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">language</span><span class="w"> </span><span class="s2">"[AUTO_LANGUAGE],en"</span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c1"># Helps get the label in your language, if not, then en language</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Get a forward mapping from all ISSNs to ISSN-L. Note that these have been
filtered to scientific journals (wd:Q5633421)</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="nv">?issn</span><span class="w"> </span><span class="nv">?issnl</span><span class="w">
</span><span class="k">WHERE</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nv">?journal</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P31</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q5633421</span><span class="w"> </span><span class="p">;</span><span class="w">
           </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P7363</span><span class="w"> </span><span class="nv">?issnl</span><span class="w"> </span><span class="p">;</span><span class="w">
           </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P236</span><span class="w"> </span><span class="nv">?issn</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">bd</span><span class="o">:</span><span class="ss">serviceParam</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">language</span><span class="w"> </span><span class="s2">"[AUTO_LANGUAGE],en"</span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c1"># Helps get the label in your language, if not, then en language</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="publishers">Publishers</h2>

<p>The following SPARQL query gets information about publishers:</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="nv">?publisher</span><span class="w"> </span><span class="nv">?publisherLabel</span><span class="w"> </span><span class="nv">?ror</span><span class="w"> </span><span class="nv">?grid</span><span class="w"> </span><span class="nv">?isni</span><span class="w">
</span><span class="k">WHERE</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nv">?publisher</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P31</span><span class="o">/</span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P279</span><span class="o">+</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q2085381</span><span class="w"> </span><span class="p">;</span><span class="w">
             </span><span class="nn">rdfs</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="nv">?publisherLabel</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">FILTER</span><span class="w"> </span><span class="p">(</span><span class="w"> </span><span class="nb">LANG</span><span class="p">(</span><span class="nv">?publisherLabel</span><span class="p">)</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s2">"en"</span><span class="w"> </span><span class="p">)</span><span class="w">
  </span><span class="k">OPTIONAL</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">?publisher</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P6782</span><span class="w"> </span><span class="nv">?ror</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="k">OPTIONAL</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">?publisher</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P2427</span><span class="w"> </span><span class="nv">?grid</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="k">OPTIONAL</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">?publisher</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P213</span><span class="w"> </span><span class="nv">?isni</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Follow <a href="https://w.wiki/6ry$">this link</a> to populate the Wikidata Query Service
with this query. This query returns the
<a href="https://bioregistry.io/registry/ror">Research Organization Registry (ROR)</a>
identifier when available. This registry effectively subsumes the
<a href="https://bioregistry.io/registry/grid">Global Research Identifier Database (GRID)</a>,
which has since been shut down, but this might be helpful for integrating data
that hasn’t been updated. The
<a href="https://bioregistry.io/registry/isni">International Standard Name Identifier (ISNI)</a>
is also included when available. Wikidata has several other nomenclature
authorities such as <a href="https://bioregistry.io/registry/gnd">GND</a>,
<a href="https://bioregistry.io/registry/viaf">VIAF</a>, RingGold, and others that are
omitted for brevity (each has their own corresponding Wikidata property.).</p>

<p>Later, I could consider adding a clause to make sure there’s a “scientific
journal” in the publisher to remove some irrelevant records.</p>

<h2 id="connections-between-journals-and-publishers">Connections between Journals and Publishers</h2>

<p>Finally, the <a href="https://bioregistry.io/wikidata:P123">publisher (P123)</a> relation
can be used to identify the relationships between journals and their respective
publishers.</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="nv">?journal</span><span class="w"> </span><span class="nv">?journalLabel</span><span class="w"> </span><span class="nv">?publisher</span><span class="w"> </span><span class="nv">?publisherLabel</span><span class="w">
</span><span class="k">WHERE</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nv">?journal</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P31</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q5633421</span><span class="w"> </span><span class="p">;</span><span class="w">
           </span><span class="nn">rdfs</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="nv">?journalLabel</span><span class="w"> </span><span class="p">;</span><span class="w">
           </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P123</span><span class="w"> </span><span class="nv">?publisher</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">FILTER</span><span class="w"> </span><span class="p">(</span><span class="w"> </span><span class="nb">LANG</span><span class="p">(</span><span class="nv">?journalLabel</span><span class="p">)</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s2">"en"</span><span class="w"> </span><span class="p">)</span><span class="w">
  </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">bd</span><span class="o">:</span><span class="ss">serviceParam</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">language</span><span class="w"> </span><span class="s2">"[AUTO_LANGUAGE],en"</span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c1"># Helps get the label in your language, if not, then en language</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="nv">?journalLabel</span><span class="w">
</span></code></pre></div></div>

<p>Follow <a href="https://w.wiki/6rz5">this link</a> to populate the Wikidata Query Service
with this query.</p>

<p>Rather than using the Wikidata label service for the journal label, I more
explicitly wrote it out to ensure that there is an english label, and to remove
anything without an english label.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="Wikidata" /><category term="bibliometrics" /><summary type="html"><![CDATA[Today’s short post is about three SPARQL queries I wrote to get bibliometric information about journals and publishers out of Wikidata.]]></summary></entry><entry><title type="html">Modeling and Querying Awards in Wikidata</title><link href="https://cthoyt.com/2023/06/08/wikidata-awards.html" rel="alternate" type="text/html" title="Modeling and Querying Awards in Wikidata" /><published>2023-06-08T10:09:23+00:00</published><updated>2023-06-08T10:09:23+00:00</updated><id>https://cthoyt.com/2023/06/08/wikidata-awards</id><content type="html" xml:base="https://cthoyt.com/2023/06/08/wikidata-awards.html"><![CDATA[<p>I was recently nominated for the International Society for Biocuration’s
<a href="https://www.biocuration.org/excellence-in-biocuration-early-career-award-2023/">Excellence in Biocuration Early Career Award</a>
(results will be announced on June 14<sup>th</sup>!). This made me curious about
how to model nominations and awards on Wikidata. In this post, I’ll describe how
to curate awards, nominations, recipients, and how to make SPARQL queries to get
them.</p>

<h2 id="summarizing-an-individual">Summarizing an Individual</h2>

<p>I’m going to use SPARQL with the
<a href="https://query.wikidata.org/">Wikidata Query Service</a> to see what’s already in
Wikidata. First, I want to find all awards that I’ve personally received using
the <a href="https://www.wikidata.org/wiki/Property:P166">P166 (award received)</a>
property. Note that the following query also takes advantage of Wikidata’s
reification so I can reach into the qualifiers of each statement to figure out
when the award was given.</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="nv">?award</span><span class="w"> </span><span class="nv">?awardLabel</span><span class="w"> </span><span class="nv">?year</span><span class="w"> </span><span class="nv">?conferer</span><span class="w"> </span><span class="nv">?confererLabel</span><span class="w">
</span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="k">VALUES</span><span class="w"> </span><span class="nv">?person</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q47475003</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="nv">?person</span><span class="w"> </span><span class="nn">p</span><span class="o">:</span><span class="ss">P166</span><span class="w"> </span><span class="nv">?award_statement</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="nv">?award_statement</span><span class="w"> </span><span class="nn">ps</span><span class="o">:</span><span class="ss">P166</span><span class="w"> </span><span class="nv">?award</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">OPTIONAL</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">?award</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P1027</span><span class="w"> </span><span class="nv">?conferer</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="k">OPTIONAL</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">?award_statement</span><span class="w"> </span><span class="nn">pq</span><span class="o">:</span><span class="ss">P585</span><span class="w"> </span><span class="nv">?date</span><span class="w"> </span><span class="p">.</span><span class="w">
    </span><span class="k">BIND</span><span class="p">(</span><span class="nb">year</span><span class="p">(</span><span class="nv">?date</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="nv">?year</span><span class="p">)</span><span class="w">
  </span><span class="p">}</span><span class="w">
  </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">bd</span><span class="o">:</span><span class="ss">serviceParam</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">language</span><span class="w"> </span><span class="s2">"[AUTO_LANGUAGE],en"</span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>See this query in action at <a href="https://w.wiki/6odU">https://w.wiki/6odU</a>.</p>

<p>As of time of writing, the only award that is listed here is the Bernie Lemire
Award. This was given to me by the Northeastern University Department of
Chemistry at the end of my bachelor’s degree for service to the department and
academic excellence. I am very proud of this award! You can switch out
<code class="language-plaintext highlighter-rouge">wd:Q47475003</code> for your Wikidata identifier.</p>

<p>A similar SPARQL query can be written to identify all of the awards for which I
was nominated by swapping the predicate to
<a href="https://www.wikidata.org/wiki/Property:P1411">P1411 (nominated for)</a>. This
isn’t necessarily a superset of the awards received since some awards are
decided without a nomination. It might also be the case depending on how
curation is done that these are out of sync.</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="nv">?award</span><span class="w"> </span><span class="nv">?awardLabel</span><span class="w"> </span><span class="nv">?year</span><span class="w"> </span><span class="nv">?conferer</span><span class="w"> </span><span class="nv">?confererLabel</span><span class="w">
</span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="k">VALUES</span><span class="w"> </span><span class="nv">?person</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q47475003</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="nv">?person</span><span class="w"> </span><span class="nn">p</span><span class="o">:</span><span class="ss">P1411</span><span class="w"> </span><span class="nv">?award_statement</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="nv">?award_statement</span><span class="w"> </span><span class="nn">ps</span><span class="o">:</span><span class="ss">P1411</span><span class="w"> </span><span class="nv">?award</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">OPTIONAL</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">?award</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P1027</span><span class="w"> </span><span class="nv">?conferer</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="k">OPTIONAL</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">?award_statement</span><span class="w"> </span><span class="nn">pq</span><span class="o">:</span><span class="ss">P585</span><span class="w"> </span><span class="nv">?date</span><span class="w"> </span><span class="p">.</span><span class="w">
    </span><span class="k">BIND</span><span class="p">(</span><span class="nb">year</span><span class="p">(</span><span class="nv">?date</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="nv">?year</span><span class="p">)</span><span class="w">
  </span><span class="p">}</span><span class="w">
  </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">bd</span><span class="o">:</span><span class="ss">serviceParam</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">language</span><span class="w"> </span><span class="s2">"[AUTO_LANGUAGE],en"</span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>See this query in action at <a href="https://w.wiki/6odV">https://w.wiki/6odV</a>.</p>

<h2 id="summarizing-an-award">Summarizing an Award</h2>

<p>Many awards are given on a periodic basis (e.g., yearly, bi-yearly).
<a href="https://scholia.toolforge.org/award">Scholia</a> is an excellent frontend to
Wikidata that already has a way of summarizing awards. Some examples:</p>

<ul>
  <li><a href="https://scholia.toolforge.org/award/Q44585">Nobel Prize in Chemistry (Q44585)</a></li>
  <li><a href="https://scholia.toolforge.org/award/Q106045191">Biocuration Career Award (Q106045191)</a></li>
</ul>

<h2 id="summarizing-a-conferrer">Summarizing a Conferrer</h2>

<p>Finally, I want to summarize all awards nominated or given by an organization.
In this example, I’m going to look at the
<a href="https://bioregistry.io/wikidata:Q23809291">International Society for Biocuration (ISB; Q23809291)</a>.</p>

<h3 id="winners">Winners</h3>

<p>The following query shows all of the recipients for all of the various awards
conferred by the ISB:</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="nv">?award</span><span class="w"> </span><span class="nv">?awardLabel</span><span class="w"> </span><span class="nv">?recipient</span><span class="w"> </span><span class="nv">?recipientLabel</span><span class="w"> </span><span class="nv">?year</span><span class="w">
</span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nv">?recipient</span><span class="w"> </span><span class="nn">p</span><span class="o">:</span><span class="ss">P166</span><span class="w"> </span><span class="nv">?award_statement</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="nv">?award_statement</span><span class="w"> </span><span class="nn">ps</span><span class="o">:</span><span class="ss">P166</span><span class="w"> </span><span class="nv">?award</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">OPTIONAL</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">?award_statement</span><span class="w"> </span><span class="nn">pq</span><span class="o">:</span><span class="ss">P585</span><span class="w"> </span><span class="nv">?date</span><span class="w"> </span><span class="p">.</span><span class="w">
    </span><span class="k">BIND</span><span class="p">(</span><span class="nb">year</span><span class="p">(</span><span class="nv">?date</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="nv">?year</span><span class="p">)</span><span class="w">
  </span><span class="p">}</span><span class="w">
  </span><span class="nv">?award</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P1027</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q23809291</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">bd</span><span class="o">:</span><span class="ss">serviceParam</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">language</span><span class="w"> </span><span class="s2">"[AUTO_LANGUAGE],en"</span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">DESC</span><span class="p">(</span><span class="nv">?year</span><span class="p">)</span><span class="w"> </span><span class="nv">?awardLabel</span><span class="w">
</span></code></pre></div></div>

<p>See this query in action at <a href="https://w.wiki/6odW">https://w.wiki/6odW</a> or the
results embedded below.</p>

<iframe style="width: 80vw; height: 50vh; border: none;" src="https://query.wikidata.org/embed.html#SELECT%20%3Faward%20%3FawardLabel%20%3Frecipient%20%3FrecipientLabel%20%3Fyear%20%0AWHERE%20%7B%20%0A%20%20%3Frecipient%20p%3AP166%20%3Faward_statement%20.%0A%20%20%3Faward_statement%20ps%3AP166%20%3Faward%20.%0A%20%20OPTIONAL%20%7B%20%0A%20%20%20%20%3Faward_statement%20pq%3AP585%20%3Fdate%20.%20%0A%20%20%20%20BIND%28year%28%3Fdate%29%20AS%20%3Fyear%29%0A%20%20%7D%0A%20%20%3Faward%20wdt%3AP1027%20wd%3AQ23809291%20.%0A%20%20SERVICE%20wikibase%3Alabel%20%7B%20bd%3AserviceParam%20wikibase%3Alanguage%20%22%5BAUTO_LANGUAGE%5D%2Cen%22.%20%7D%0A%7D%0AORDER%20BY%20DESC%28%3Fyear%29%20%3FawardLabel" referrerpolicy="origin" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>

<p>At the time of writing, this only returned a paltry 9 rows, meaning more
curation is necessary! Considering this award is about biocurators, we better
get our act together 🙃. <strong>Update June 4<sup>th</sup>, 2023:</strong> I went back and
curated the full catalog.</p>

<h3 id="nominations">Nominations</h3>

<p>Similarly, the following query can be used to identify all nominations:</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="nv">?award</span><span class="w"> </span><span class="nv">?awardLabel</span><span class="w"> </span><span class="nv">?nominee</span><span class="w"> </span><span class="nv">?nomineeLabel</span><span class="w"> </span><span class="nv">?year</span><span class="w">
</span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nv">?nominee</span><span class="w"> </span><span class="nn">p</span><span class="o">:</span><span class="ss">P1411</span><span class="w"> </span><span class="nv">?award_statement</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="nv">?award_statement</span><span class="w"> </span><span class="nn">ps</span><span class="o">:</span><span class="ss">P1411</span><span class="w"> </span><span class="nv">?award</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">OPTIONAL</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">?award_statement</span><span class="w"> </span><span class="nn">pq</span><span class="o">:</span><span class="ss">P585</span><span class="w"> </span><span class="nv">?date</span><span class="w"> </span><span class="p">.</span><span class="w">
    </span><span class="k">BIND</span><span class="p">(</span><span class="nb">year</span><span class="p">(</span><span class="nv">?date</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="nv">?year</span><span class="p">)</span><span class="w">
  </span><span class="p">}</span><span class="w">
  </span><span class="nv">?award</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P1027</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q23809291</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">bd</span><span class="o">:</span><span class="ss">serviceParam</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">language</span><span class="w"> </span><span class="s2">"[AUTO_LANGUAGE],en"</span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">DESC</span><span class="p">(</span><span class="nv">?year</span><span class="p">)</span><span class="w"> </span><span class="nv">?awardLabel</span><span class="w">
</span></code></pre></div></div>

<p>See this query in action at <a href="https://w.wiki/6odX">https://w.wiki/6odX</a> or the
results embedded below.</p>

<iframe style="width: 80vw; height: 50vh; border: none;" src="https://query.wikidata.org/embed.html#SELECT%20%3Faward%20%3FawardLabel%20%3Fnominee%20%3FnomineeLabel%20%3Fyear%20%0AWHERE%20%7B%20%0A%20%20%3Fnominee%20p%3AP1411%20%3Faward_statement%20.%0A%20%20%3Faward_statement%20ps%3AP1411%20%3Faward%20.%0A%20%20OPTIONAL%20%7B%20%0A%20%20%20%20%3Faward_statement%20pq%3AP585%20%3Fdate%20.%20%0A%20%20%20%20BIND%28year%28%3Fdate%29%20AS%20%3Fyear%29%0A%20%20%7D%0A%20%20%3Faward%20wdt%3AP1027%20wd%3AQ23809291%20.%0A%20%20SERVICE%20wikibase%3Alabel%20%7B%20bd%3AserviceParam%20wikibase%3Alanguage%20%22%5BAUTO_LANGUAGE%5D%2Cen%22.%20%7D%0A%7D%0AORDER%20BY%20DESC%28%3Fyear%29%20%3FawardLabel" referrerpolicy="origin" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>

<p>There are only 5 results at the time of writing, and these are for my fellow
nominees for the Excellence in Biocuration Early Career Award that I recently
curated! There’s a lot of work to do here for keeping a history of the ISB’s
awards. <strong>Update June 4<sup>th</sup>, 2023:</strong> I went back and curated the full
catalog. It turns out that the ISB did not publish the list of nominees for any
awards until 2022, so this list will remain short.</p>

<h3 id="wikidata-wide-summary">Wikidata-wide Summary</h3>

<p>More generally, it turns out that there are only a bit more than 55K nomination
relations in total for all of Wikidata. You can check this with:</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="p">(</span><span class="nb">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="nv">?count</span><span class="p">)</span><span class="w">
</span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">?nominee</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P1411</span><span class="w"> </span><span class="nv">?award</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="curating-an-award">Curating an Award</h2>

<p>Award objects don’t have to be complicated - the most important information is
to include a useful instance annotation (e.g., to
<a href="https://www.wikidata.org/wiki/Q11448906">science award (Q11448906)</a>) and the
following:</p>

<ol>
  <li>Field of Work (P101)</li>
  <li>Conferred By (P1027)</li>
  <li>Website (P856)</li>
  <li>Inception (P571)</li>
</ol>

<p>See
<a href="https://www.wikidata.org/wiki/Q118947746">https://www.wikidata.org/wiki/Q118947746</a>
as an example.</p>

<h2 id="curating-an-individual">Curating an Individual</h2>

<p>On a given Wikidata page, you can add a statement for either <em>nominated for</em> or
<em>award received</em> using Wikidata’s amazing curation interface that has search
built in. It’s recommended to add a
<a href="https://www.wikidata.org/wiki/Property:P585">point in time (P585)</a> annotation
to make a distinction between different periods. Further, it’s recommended to
add a refernce using the
<a href="https://www.wikidata.org/wiki/Property:P854">reference url (P854)</a> property
that pints to a webpage with an announcement about the nomination or award.</p>

<p><img src="/img/wikidata_add_nominee_1.png" alt="" /></p>

<hr />

<p>Overall, I think modeling awards is hard, since these are less concrete than
other academic information such as employment or education. Still, this is the
next step in making my resume 100% auto-generated by SPARQL and Wikidata!</p>

<p>See also <a href="https://github.com/cmungall/science-award-analysis">an analysis</a> by
Chris Mungall of the gender distribution of awards in Wikidata.</p>

<p><strong>Update June 4<sup>th</sup>, 2023:</strong> I won the Excellence in Biocuration Early
Career Award! Nico Matentzoglu won the Excellence in Biocuration Advanced Career
Award and we are both excited to see that the community was interested to
recognize people who work on fundamental underlying technologies.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="Wikidata" /><category term="bibliometrics" /><summary type="html"><![CDATA[I was recently nominated for the International Society for Biocuration’s Excellence in Biocuration Early Career Award (results will be announced on June 14th!). This made me curious about how to model nominations and awards on Wikidata. In this post, I’ll describe how to curate awards, nominations, recipients, and how to make SPARQL queries to get them.]]></summary></entry><entry><title type="html">Re-implementing the N2T ARK Resolver</title><link href="https://cthoyt.com/2023/04/11/n2t-ark-resolver.html" rel="alternate" type="text/html" title="Re-implementing the N2T ARK Resolver" /><published>2023-04-11T18:44:23+00:00</published><updated>2023-04-11T18:44:23+00:00</updated><id>https://cthoyt.com/2023/04/11/n2t-ark-resolver</id><content type="html" xml:base="https://cthoyt.com/2023/04/11/n2t-ark-resolver.html"><![CDATA[<p><a href="https://arks.org/">Archival Resource Keys (ARKs)</a> are flavor of persistent
identifiers like DOIs, URNs, and Handles that have the benefit of being free,
flexible with what metadata gets attached, and natively able to resolve to web
pages. <a href="https://n2t.net">Name-to-Thing (N2T)</a> implements a resolver for a
variety of ARKs, so this blog post is about how that resolver can be
re-implemented with the <a href="https://github.com/cthoyt/curies/"><code class="language-plaintext highlighter-rouge">curies</code></a> Python
package.</p>

<p>In a lot of ways, ARKs look and act like CURIEs. For example,
<code class="language-plaintext highlighter-rouge">ark:/53355/cl010277627</code> could be interpreted as having the prefix <code class="language-plaintext highlighter-rouge">ark</code> and the
local unique identifier <code class="language-plaintext highlighter-rouge">/53355/cl010277627</code>. The first part of each ARK between
the first two slashes corresponds to the provider. In this example, <code class="language-plaintext highlighter-rouge">53355</code>
corresponds to the <a href="https://www.louvre.fr/en">Louvre</a> museum in Paris, France
and <code class="language-plaintext highlighter-rouge">cl010277627</code> is the local unique identifier corresponding to the Vénus de
Milo statue.</p>

<p>However, I might have just committed ARK blasphemy. In N2T, it appears that the
ARK prefix and provider code stay grouped together in the front half like
<code class="language-plaintext highlighter-rouge">ark:/53355/</code> and then the back half <code class="language-plaintext highlighter-rouge">cl010277627</code> represents the local unique
identifier. This is very similar to the two-layer identifiers in DOI and the
arbitrary number of layer identifiers in OID.</p>

<p>The point is, if we can interpret this enough like CURIEs, we can use the
<code class="language-plaintext highlighter-rouge">curies</code> package to implement a resolver. The first step we can take is to
download the N2T data from
<a href="https://n2t.net/e/n2t_full_prefixes.yaml">https://n2t.net/e/n2t_full_prefixes.yaml</a>.
Then we can parse out the ARKs (there are other things in N2T we’ll disregard)
with the following code:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pystow</span>
<span class="kn">import</span> <span class="nn">yaml</span>

<span class="n">URL</span> <span class="o">=</span> <span class="s">"https://n2t.net/e/n2t_full_prefixes.yaml"</span>
<span class="n">PROTOCOLS</span> <span class="o">=</span> <span class="p">{</span><span class="s">"https://"</span><span class="p">,</span> <span class="s">"http://"</span><span class="p">,</span> <span class="s">"ftp://"</span><span class="p">}</span>


<span class="k">def</span> <span class="nf">get_prefix_map</span><span class="p">():</span>
    <span class="s">"""Get the prefix map from N2T, not including redundant ``ark:/`` in prefixes."""</span>
    <span class="k">with</span> <span class="n">pystow</span><span class="p">.</span><span class="n">ensure_open</span><span class="p">(</span><span class="s">"n2t"</span><span class="p">,</span> <span class="n">url</span><span class="o">=</span><span class="n">URL</span><span class="p">)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
        <span class="n">records</span> <span class="o">=</span> <span class="n">yaml</span><span class="p">.</span><span class="n">safe_load</span><span class="p">(</span><span class="nb">file</span><span class="p">)</span>
    <span class="n">prefix_map</span> <span class="o">=</span> <span class="p">{}</span>
    <span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">record</span> <span class="ow">in</span> <span class="n">records</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
        <span class="n">uri_prefix</span> <span class="o">=</span> <span class="n">record</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"redirect"</span><span class="p">)</span>
        <span class="k">if</span> <span class="p">(</span>
            <span class="ow">not</span> <span class="n">uri_prefix</span>
            <span class="ow">or</span> <span class="nb">all</span><span class="p">(</span><span class="ow">not</span> <span class="n">uri_prefix</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="n">protocol</span><span class="p">)</span> <span class="k">for</span> <span class="n">protocol</span> <span class="ow">in</span> <span class="n">PROTOCOLS</span><span class="p">)</span>
            <span class="ow">or</span> <span class="n">uri_prefix</span><span class="p">.</span><span class="n">count</span><span class="p">(</span><span class="s">"$id"</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">1</span>
            <span class="ow">or</span> <span class="ow">not</span> <span class="n">uri_prefix</span><span class="p">.</span><span class="n">endswith</span><span class="p">(</span><span class="s">"$id"</span><span class="p">)</span>
            <span class="ow">or</span> <span class="ow">not</span> <span class="n">key</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="s">"ark:/"</span><span class="p">)</span>
        <span class="p">):</span>
            <span class="k">continue</span>
        <span class="n">key</span> <span class="o">=</span> <span class="n">key</span><span class="p">.</span><span class="n">removeprefix</span><span class="p">(</span><span class="s">"ark:/"</span><span class="p">)</span>
        <span class="n">prefix_map</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">uri_prefix</span><span class="p">.</span><span class="n">removesuffix</span><span class="p">(</span><span class="s">"$id"</span><span class="p">)</span> <span class="o">+</span> <span class="s">"/"</span> <span class="o">+</span> <span class="n">key</span> <span class="o">+</span> <span class="s">"/"</span>
    <span class="k">return</span> <span class="n">prefix_map</span>
</code></pre></div></div>

<p>This prefix map removes <code class="language-plaintext highlighter-rouge">ark:/</code> from the beginning of the prefixes in N2T and
also adds the provider code into the URI prefix to make the URIs more focused on
the local unique identifiers within each provider, rather than the entire ARK
space.</p>

<p>Once we have a prefix map, we can make a <code class="language-plaintext highlighter-rouge">curies.Converter</code> and a Flask web
application for resolving in a few lines:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">curies</span> <span class="kn">import</span> <span class="n">Converter</span><span class="p">,</span> <span class="n">get_flask_app</span>


<span class="k">def</span> <span class="nf">get_app</span><span class="p">():</span>
    <span class="s">"""Get an ARK resolver app, noting that it uses a non-standard delimiter and URL prefix."""</span>
    <span class="n">prefix_map</span> <span class="o">=</span> <span class="n">get_prefix_map</span><span class="p">()</span>
    <span class="k">print</span><span class="p">(</span><span class="n">prefix_map</span><span class="p">)</span>
    <span class="n">converter</span> <span class="o">=</span> <span class="n">Converter</span><span class="p">.</span><span class="n">from_prefix_map</span><span class="p">(</span><span class="n">prefix_map</span><span class="p">,</span> <span class="n">delimiter</span><span class="o">=</span><span class="s">"/"</span><span class="p">)</span>
    <span class="n">app</span> <span class="o">=</span> <span class="n">get_flask_app</span><span class="p">(</span><span class="n">converter</span><span class="p">,</span> <span class="n">blueprint_kwargs</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="n">url_prefix</span><span class="o">=</span><span class="s">"/ark:"</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">app</span>
</code></pre></div></div>

<p>The two tricks here are:</p>

<ol>
  <li>We want to remove the redundant <code class="language-plaintext highlighter-rouge">ark:/</code> then interpret the ARK provider code
as the prefix and the rest as the local unique identifier. However, we still
want to be able to write URLs in our resolver that have the <code class="language-plaintext highlighter-rouge">ark:/</code> prefix.
Luckily, Flask has the facility to define a default <code class="language-plaintext highlighter-rouge">url_prefix</code> before a
given blueprint that we invoke directly.</li>
  <li>Unlike CURIEs that use a colon <code class="language-plaintext highlighter-rouge">:</code> as the delimiter between the prefix and
local unique identifier, ARKs use a slash <code class="language-plaintext highlighter-rouge">/</code>. We can also set this in the
Converter’s settings.</li>
</ol>

<p>Now, all we need to do is instantiate the app and serve it with any WSGI tool
like Gunicorn, Uvicorn, or Flask’s built-in development server (from Werkzeug).
Navigating to http://localhost:5000/ark:/53355/cl010277627 redirects to
https://collections.louvre.fr/ark:/53355/cl010277627 and gets some nice art from
the Louvre. In general, you can stick any ARK after http://localhost:5000/ark:
that is resolvable via N2T when running this server.</p>

<p>All of this code is on
<a href="https://github.com/cthoyt/n2t-ark-resolver/tree/main">GitHub</a> and can be run
with the following:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/cthoyt/n2t-ark-resolver
<span class="nb">cd </span>n2t-ark-resolver
python <span class="nt">-m</span> pip <span class="nb">install</span> <span class="nt">-r</span> requirements.txt
python wsgi.py
</code></pre></div></div>

<hr />

<p>Update: since posting this, I have heard from John Kunze that the ARK format is
currently being updated to look more like URNs and therefore not have the slash
after <code class="language-plaintext highlighter-rouge">ark:/</code> anymore. If/when that happens, there are only a few bits of string
pre-processing in this script that need to be updated to keep everything
running.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="CURIEs" /><category term="ARKs" /><category term="N2T" /><category term="PIDs" /><summary type="html"><![CDATA[Archival Resource Keys (ARKs) are flavor of persistent identifiers like DOIs, URNs, and Handles that have the benefit of being free, flexible with what metadata gets attached, and natively able to resolve to web pages. Name-to-Thing (N2T) implements a resolver for a variety of ARKs, so this blog post is about how that resolver can be re-implemented with the curies Python package.]]></summary></entry><entry><title type="html">The Representatives of Monkey Jack - German Battle of the Bands Finale</title><link href="https://cthoyt.com/2023/03/29/monkey-jack-lmh.html" rel="alternate" type="text/html" title="The Representatives of Monkey Jack - German Battle of the Bands Finale" /><published>2023-03-29T19:33:23+00:00</published><updated>2023-03-29T19:33:23+00:00</updated><id>https://cthoyt.com/2023/03/29/monkey-jack-lmh</id><content type="html" xml:base="https://cthoyt.com/2023/03/29/monkey-jack-lmh.html"><![CDATA[<p>This blog is normally about <strong>very serious science</strong>, but I’m
taking a break from that for the evening to advertize my band’s upcoming show on
April 8<sup>th</sup> in the SPH Music Masters Finale (aka, the German Battle of
the Bands). We need your support! There are streaming tickets available, and
this post has a guide on how to navigate the German website to get tickets (or
just text me, I’ll hook you up).</p>

<p>Here’s a video of us playing. If you love it, let me know. If you hate it, slap
like and subscribe.</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/P1OXgfN8xIU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen=""></iframe>

<h2 id="when-is-this">When is this?</h2>

<p>The show is on April 8<sup>th</sup> at the Live Music Hall in Cologne, Germany.
Because this is Easter weekend, it’s the perfect thing to do while you’re
relaxing with your family. The show starts at 15.30 CEST / 9:30AM EST. The order
of the 10 bands playing will be determined on the morning of, so I’ll send out a
mass text about what time that will be for everyone who’s streaming.</p>

<h2 id="how-does-the-battle-work">How does the battle work?</h2>

<p>The battle of the bands is judged in two ways: half of the score is by the
judges and half by the audience. Each audience member gets two votes - this
usually means they vote for the band they support and a second band that they
liked after seeing them for the first time.</p>

<p>There will be 10 bands playing, which means this is going to be a loooooong day.
The best spots in the show will be in the middle or towards the end, after the
afternoon settles into evening. The order will be determined just before the
show starts, based on which band sells the most pre-sale tickets. This means
that getting a streaming ticket will support us to get a better spot, which is
highly correlated with winning.</p>

<h2 id="who-is-monkey-jack-and-why-do-you-represent-him">Who is Monkey Jack, and why do you represent him?</h2>

<p>As you noticed, we’re the <i>representatives</i> of Monkey Jack. We will tell
The Tale of Monkey Jack at our show. You should prepare by bringing a banana
with you to the show, which you will need at the end of the Tale when we begin
<i>The Ritual</i>.</p>

<h2 id="getting-a-ticket">Getting a Ticket</h2>

<p>First, navigate <a href="https://www.sph-music-masters.de/tickets/index/3765">here</a> for
tickets.</p>

<p>Second, click <i>Streamingtickets</i>. If you want to be cool while speaking
German, you should throw in some English words (or internationalisms).</p>

<p><img src="/img/monkey-jack-lmh/step1.png" alt="" /></p>

<p>Third, click <i>Auswählen</i>. This verb means that you are pledging your
allegiance to Monkey Jack and promise to follow <strong><i>Him</i></strong>.</p>

<p><img src="/img/monkey-jack-lmh/step2.png" alt="" /></p>

<p>Fourth, click <i>Bitte Wählen</i> (please choose). This is a drop down menu to
show your support for Monkey Jack and his representatives. Note that you only
need one streaming ticket per stream, obviously you should throw a party/ritual
to represent Monkey Jack yourself.</p>

<p><img src="/img/monkey-jack-lmh/step3.png" alt="" /></p>

<p>Fifth, click <i>Monkey Jack</i>.</p>

<p><img src="/img/monkey-jack-lmh/step4.png" alt="" /></p>

<p>Sixth, You can fill in the form with your information. The image below annotates
what each of the fields means. Plz is short for “Postleitzahl”, which means Zip
code. Don’t worry about the part of the form with the country picker. You’re a
German now.</p>

<p><img src="/img/monkey-jack-lmh/step5.png" alt="" /></p>

<p>After you click it, it will bring you to a PayPal page. They’ll email you a
confirmation within 5-10 minutes and send the streaming link the day of the
show.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="music" /><summary type="html"><![CDATA[This blog is normally about very serious science, but I’m taking a break from that for the evening to advertize my band’s upcoming show on April 8th in the SPH Music Masters Finale (aka, the German Battle of the Bands). We need your support! There are streaming tickets available, and this post has a guide on how to navigate the German website to get tickets (or just text me, I’ll hook you up).]]></summary></entry><entry><title type="html">Resources masquerading as OBO Foundry ontologies</title><link href="https://cthoyt.com/2023/03/11/obolibrary-masquerade.html" rel="alternate" type="text/html" title="Resources masquerading as OBO Foundry ontologies" /><published>2023-03-11T23:11:23+00:00</published><updated>2023-03-11T23:11:23+00:00</updated><id>https://cthoyt.com/2023/03/11/obolibrary-masquerade</id><content type="html" xml:base="https://cthoyt.com/2023/03/11/obolibrary-masquerade.html"><![CDATA[<p>Several controlled vocabularies and ontologies that aren’t themselves
<a href="https://obofoundry.org/">OBO Foundry</a> ontologies use unsanctioned OBO PURLs.
This post is about how to use <a href="https://bioregistry.io/">the Bioregistry</a> to
identify which resources are doing this and to give some insight into how we
arrived in this situation.</p>

<h2 id="background-on-the-obo-foundry">Background on the OBO Foundry</h2>

<p>The <a href="https://obofoundry.org/">OBO Foundry</a> is a set of independent,
interoperable biomedical ontologies that aspire to using
<a href="https://obofoundry.org/principles/fp-000-summary.html">shared development principles</a>.
One such principle is to use a principled approach for creating persistent
uniform resource locators (PURLs) for local unique identifiers in each ontology.
These PURLs follow the form
<code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/&lt;PREFIX&gt;_&lt;LOCAL UNIQUE IDENTIFIER&gt;</code>. For
example, a prefix might be <code class="language-plaintext highlighter-rouge">GO</code> (for the Gene Ontology) and local unique
identifier might be <code class="language-plaintext highlighter-rouge">0032571</code> (for <em>response to vitamin K</em> in GO) resulting in
the PURL
<a href="http://purl.obolibrary.org/obo/GO_0032571">http://purl.obolibrary.org/obo/GO_0032571</a>.</p>

<p>While most semantic web resources allow the use of any IRIs (internationalized
resource identifiers), the OBO Foundry enforces that its PURLs resolve to
something useful for readers (e.g., to the
<a href="https://www.ebi.ac.uk/ols">Ontology Lookup Service</a>). The resolver behind
<a href="http://purl.obolibrary.org">http://purl.obolibrary.org</a> is implemented and
maintained in a
<a href="https://github.com/OBOFoundry/purl.obolibrary.org/">GitHub repository</a> with
corresponding <code class="language-plaintext highlighter-rouge">.htaccess</code> files for each OBO Foundry ontology. Correct and
useful configuration for each ontology are a requirement for acceptance to the
OBO Foundry.</p>

<p>At the core of the OBO Foundry are several high quality, well-known, generally
useful ontologies such as the <a href="https://bioregistry.io/go">Gene Ontology</a> and the
<a href="https://bioregistry.io/go">Cell Ontology</a>. Inclusion in the OBO Foundry has
therefore become a <em>de facto</em> stamp of approval for ontologies that (until now)
254 ontologies have (for better or worse) successfully sought out.</p>

<h2 id="the-masquerade">The Masquerade</h2>

<p>Unfortunately, some ontologies and controlled vocabularies have adopted OBO
PURLs even though they are not OBO Foundry ontologies. This is a problem for a
few reasons:</p>

<ol>
  <li>The PURLs are not registered in
<a href="https://github.com/OBOFoundry/purl.obolibrary.org/">https://github.com/OBOFoundry/purl.obolibrary.org/</a>
and therefore do not resolve</li>
  <li>The quality of the ontology has not been vetted, meaning that low-quality
ontologies using OBO PURLs could damage community trust in the OBO Foundry</li>
  <li>These PURLs might conflict with other URIs prefix synonyms, increasing
complexity for data integration</li>
  <li>Most importantly: it’s confusing.</li>
</ol>

<p>One of the jobs of <a href="https://bioregistry.io/">the Bioregistry</a> is to catalog the
URI format strings for identifier resources useful for the life and natural
sciences. This allows us to assess how big the problem of non-OBO Foundry
ontologies are using OBO PURLs, and why. Without further ado, here’s the list of
offending resources that appear in the Bioregistry:</p>

<table>
  <thead>
    <tr>
      <th>prefix</th>
      <th>name</th>
      <th>evidence</th>
      <th>uri_prefix</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://bioregistry.io/aeon">aeon</a></td>
      <td>Academic Event Ontology</td>
      <td>curated</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/AEON_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/cemo">cemo</a></td>
      <td>COVID-19 epidemiology and monitoring ontology</td>
      <td>extra</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/cemo.owl#</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/covoc">covoc</a></td>
      <td>CoVoc Coronavirus Vocabulary</td>
      <td>curated</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/COVOC_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/decipher">decipher</a></td>
      <td>DECIPHER CNV Syndromes</td>
      <td>biocontext</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/DECIPHER_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/dermo">dermo</a></td>
      <td>Human Dermatological Disease Ontology</td>
      <td>curated</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/DERMO_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/efo">efo</a></td>
      <td>Experimental Factor Ontology</td>
      <td>biocontext</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/EFO_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/gorel">gorel</a></td>
      <td>GO Relations</td>
      <td>biolink</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/GOREL_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/hpath">hpath</a></td>
      <td>Histopathology Ontology</td>
      <td>curated</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/MC_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/idocovid19">idocovid19</a></td>
      <td>COVID-19 Infectious Disease Ontology</td>
      <td>curated</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/COVIDO_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/lbo">lbo</a></td>
      <td>Livestock Breed Ontology</td>
      <td>curated</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/LBO_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/lpt">lpt</a></td>
      <td>Livestock Product Trait Ontology</td>
      <td>curated</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/LPT_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/mesh">mesh</a></td>
      <td>Medical Subject Headings</td>
      <td>biocontext</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/MESH_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/msio">msio</a></td>
      <td>Metabolomics Standards Initiative Ontology</td>
      <td>curated</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/MSIO_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/omia">omia</a></td>
      <td>Online Mendelian Inheritance in Animals</td>
      <td>biocontext</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/OMIA_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/omim">omim</a></td>
      <td>Online Mendelian Inheritance in Man</td>
      <td>biocontext</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/OMIM_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/pride">pride</a></td>
      <td>PRIDE Controlled Vocabulary</td>
      <td>curated</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/PRIDE_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/reo">reo</a></td>
      <td>Reagent Ontology</td>
      <td>curated</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/REO_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/roleo">roleo</a></td>
      <td>Role Ontology</td>
      <td>curated</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/RoleO_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/soybase">soybase</a></td>
      <td>SoyBase</td>
      <td>prefixcommons</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/uniprot.isoform">uniprot.isoform</a></td>
      <td>UniProt Isoform</td>
      <td>extra</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/UniProtKB_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/vido">vido</a></td>
      <td>Virus Infectious Disease Ontology</td>
      <td>curated</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/VIDO_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/vsmo">vsmo</a></td>
      <td>Ontology for vector surveillance and management</td>
      <td>curated</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/VSMO_</code></td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/xl">xl</a></td>
      <td>Cross-linker reagents ontology</td>
      <td>curated</td>
      <td><code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/XL_</code></td>
    </tr>
  </tbody>
</table>

<p>In the <em>evidence</em> column, there are a few possible entries:</p>

<ol>
  <li><strong>curated</strong> - this is the URI prefix manually curated in the Bioregistry.
This happens when the primary ontology artifact uses OBO PURLs.</li>
  <li><strong>extra</strong> - this is when there’s a manually curated extra URI prefix in the
Bioregistry (in addition to the primary one) that uses an OBO PURL. This
usually is done to enable the Bioregistry’s IRI parser to handle cases that
appear in third-party data that incorrectly constructs IRIs.</li>
  <li><strong>biocontext</strong>, <strong>biolink</strong>, and <strong>prefixcommons</strong> - this is when other
registries have assigned OBO PURLs as their URI expansions</li>
</ol>

<p>It’s worth noting that there are probably <em>lots</em> more resources doing this,
e.g., that are listed in <a href="https://bioportal.bioontology.org/">BioPortal</a>, but
have not been included in the Bioregistry because of their lack of notability,
utility, or reuse.</p>

<details><summary>Here's the code that generated the table (before minor modifications)</summary>

```python
import bioregistry
from tabulate import tabulate

OBOLIBRARY_SUBSTRING = "purl.obolibrary.org/obo"
rows = []
for prefix, resource in bioregistry.read_registry().items():
    if resource.get_obofoundry_prefix() or prefix == "obo":
        continue
    name = resource.get_name()
    contact = resource.get_contact()
    l = f"[{prefix}](https://bioregistry.io/{prefix})"
    if resource.uri_format and OBOLIBRARY_SUBSTRING in resource.uri_format:
        rows.append((l, name, "curated", "", resource.uri_format))
        continue
    elif (uri_format := resource.get_uri_format()) and OBOLIBRARY_SUBSTRING in uri_format:
        rows.append((l, name, "default", "", uri_format))
        continue
    for metaprefix in resource.get_mappings():
        uri_format = (getattr(resource, metaprefix, None) or {}).get("uri_format")
        if uri_format and OBOLIBRARY_SUBSTRING in uri_format:
            rows.append((l, name, "mapped", metaprefix, uri_format))
    for p in resource.get_extra_providers():
        if OBOLIBRARY_SUBSTRING in p.uri_format:
            rows.append((l, name, "extra", p.code, p.uri_format))

print(tabulate(rows, headers=["prefix", "name", "type", "code", "uri_format"], tablefmt="github"))
```

</details>

<h2 id="rationalizations">Rationalizations</h2>

<p>Based on the table above, there are several situations in which an OBO PURL
appears:</p>

<ol>
  <li>Ontologies that are curated in the OBO flat file format then converted to the
OWL format using <a href="https://robot.obolibrary.org/">ROBOT</a> are automatically
given OBO PURLs. For example, this occurs for the Livestock Breeding
Ontology.</li>
  <li>Similarly, ontologies that are uploaded to BioPortal likely undergo a similar
procedure that results in BioPortal PURLs that themselves include OBO PURLs.
For example, this occurs for FamPlex and the Vital Sign Ontology.</li>
  <li>Ontologies are/were intended to be submitted to the OBO Foundry. For example,
this includes:
    <ul>
      <li>The Academic Event Ontology (AEON) is still a work in progress and will be
submitted to the OBO Foundry</li>
      <li>The Reagent Ontology (REO) was abandoned and never submitted to the OBO
Foundry</li>
    </ul>
  </li>
  <li>Ontologies that closely used by the OBO Foundry ecosystem, and are somtimes
mistaken for being in it (e.g., EFO)</li>
  <li>Not all ontologies, controlled vocabularies, or other semantic spaces have
associated PURLs. Several registries (e.g., Prefix Commons, BioContext,
BioLink Model) have “made up” OBO PURLs for non-OBO Foundry resources because
of their use case-specific preferences.</li>
  <li>In the case of XL, I think that this was an OBO Foundry ontology at some
point but got renamed. It’s very difficult to understand the history of the
<a href="http://www.psidev.info/groups/controlled-vocabularies">HUPO Proteomics Standards Initiative</a>
from the outside</li>
  <li>Some of these examples that appear with evidence “extra” are there because
third-party resources incorrectly reference entities using unsanctioned OBO
PURLs</li>
  <li>In the case of CEMO, it appears the URI prefix is an artifact of incorrect
configuration curation tooling (likely Protege).</li>
</ol>

<p>It’s hard to know for sure for the situation that lead to the
developers/maintainers of primary resources using unsanctioned OBO PURLs or the
developers/maintainers of third party resources using unsanctioned OBO PURLs.
Regardless, it’s still valuable for the community to know about these problems
and potentially use comprehensive resources like the Bioregistry as a guide
towards improving interoperability and interpretability.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="Bioregistry" /><category term="OBO" /><category term="ontologies" /><summary type="html"><![CDATA[Several controlled vocabularies and ontologies that aren’t themselves OBO Foundry ontologies use unsanctioned OBO PURLs. This post is about how to use the Bioregistry to identify which resources are doing this and to give some insight into how we arrived in this situation.]]></summary></entry><entry><title type="html">Compliance of Bioregistry Prefixes to the W3C Standard</title><link href="https://cthoyt.com/2023/01/11/bioregistry-w3c-compliance.html" rel="alternate" type="text/html" title="Compliance of Bioregistry Prefixes to the W3C Standard" /><published>2023-01-11T23:17:00+00:00</published><updated>2023-01-11T23:17:00+00:00</updated><id>https://cthoyt.com/2023/01/11/bioregistry-w3c-compliance</id><content type="html" xml:base="https://cthoyt.com/2023/01/11/bioregistry-w3c-compliance.html"><![CDATA[<p>This post gives a brief background on the formal definition of the syntax and
semantics of
<a href="https://www.w3.org/TR/2010/NOTE-curie-20101216/#s_syntax">compact uniform resource identifiers (CURIEs)</a>
from the <a href="https://www.w3.org">Worldwide Web Consortium (W3C)</a> and investigates
how many prefixes in the <a href="https://bioregistry.io">Bioregistry</a> are compliant
with the standard.</p>

<h2 id="syntax">Syntax</h2>

<p>The W3C’s
<a href="https://www.w3.org/TR/2010/NOTE-curie-20101216/#s_syntax">CURIE 1.0 Syntax</a> is
unfortunately obfuscated. Understanding it requires navigating through several
pages and reading cryptic definitions in a
<a href="https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form">BNF</a>-like notation.
Below is a short explanation of the two important parts and a nice
simplification:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>safe_curie  :=  '[' curie ']'
curie       :=  [ [ prefix ] ':' ] reference
prefix      :=  NCName
reference   :=  irelative-ref
</code></pre></div></div>

<p>where <code class="language-plaintext highlighter-rouge">NCName</code> is defined
<a href="http://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-NCName">on this page</a> as</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NCName     ::= (Letter | '_') (NCNameChar)*
NCNameChar ::= Letter | Digit  | '.' | '-' | '_' | CombiningChar | Extender
</code></pre></div></div>

<p>and <code class="language-plaintext highlighter-rouge">irelative-ref</code> is defined
<a href="https://www.w3.org/TR/2010/NOTE-curie-20101216/#ref_IRI">here</a> by referencing
external <a href="http://www.ietf.org/rfc/rfc3987.txt">RFC 3987</a>. Understanding this
part is not strictly necessary for checking Bioregistry prefixes.</p>

<p>After unpacking all of these nested references and making the reasonable
assumption that the strange characters referenced by <code class="language-plaintext highlighter-rouge">CombiningChar</code> and
<code class="language-plaintext highlighter-rouge">Extender</code> are unlikely to appear in any real prefixes, we arrive a the
following regular expression for validating prefixes:
<code class="language-plaintext highlighter-rouge">^[a-zA-Z_][a-zA-Z0-9.-_]*</code></p>

<h2 id="bioregistry-compliance">Bioregistry Compliance</h2>

<p>It’s relatively easy to write a script that checks Bioregistry prefixes against
this regular expression.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">re</span>
<span class="kn">import</span> <span class="nn">bioregistry</span>
<span class="kn">from</span> <span class="nn">tabulate</span> <span class="kn">import</span> <span class="n">tabulate</span>

<span class="n">W3C_PREFIX</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nb">compile</span><span class="p">(</span><span class="s">"^[a-zA-Z_][a-zA-Z0-9.-_]*"</span><span class="p">)</span>
<span class="n">failed</span> <span class="o">=</span> <span class="p">[</span>
    <span class="p">(</span>
        <span class="sa">f</span><span class="s">"[</span><span class="si">{</span><span class="n">resource</span><span class="p">.</span><span class="n">prefix</span><span class="si">}</span><span class="s">](https://bioregistry.io/</span><span class="si">{</span><span class="n">resource</span><span class="p">.</span><span class="n">prefix</span><span class="si">}</span><span class="s">)"</span><span class="p">,</span>
        <span class="n">resource</span><span class="p">.</span><span class="n">get_name</span><span class="p">(),</span>
    <span class="p">)</span>
    <span class="k">for</span> <span class="n">resource</span> <span class="ow">in</span> <span class="n">bioregistry</span><span class="p">.</span><span class="n">resources</span><span class="p">()</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">W3C_PREFIX</span><span class="p">.</span><span class="n">match</span><span class="p">(</span><span class="n">resource</span><span class="p">.</span><span class="n">prefix</span><span class="p">)</span>
<span class="p">]</span>
<span class="k">print</span><span class="p">(</span><span class="n">tabulate</span><span class="p">(</span><span class="n">failed</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="p">[</span><span class="s">"prefix"</span><span class="p">,</span> <span class="s">"name"</span><span class="p">],</span> <span class="n">tablefmt</span><span class="o">=</span><span class="s">"github"</span><span class="p">))</span>
</code></pre></div></div>

<p>This script produces the following table as an output:</p>

<table>
  <thead>
    <tr>
      <th>prefix</th>
      <th>name</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://bioregistry.io/3dmet">3dmet</a></td>
      <td>3D Metabolites</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/4dn.biosource">4dn.biosource</a></td>
      <td>4D Nucleome Data Portal Biosource</td>
    </tr>
    <tr>
      <td><a href="https://bioregistry.io/4dn.replicate">4dn.replicate</a></td>
      <td>4D Nucleome Data Portal Experiment Replicate</td>
    </tr>
  </tbody>
</table>

<p>Note that only three prefixes (at the time of writing) are non-compliant, each
because it starts with a number instead of a letter or underscore. Overall, the
Bioregistry is doing pretty good! Note that this does not check preferred
prefixes nor synonyms. This might be good for a future update to this post or a
follow-up post.</p>

<hr />

<p>In the future, it might be nice to enforce some kind of prefix compliance at the
unit test level to automate checking prefixes are appropriate. This might also
include a blacklist of certain generic prefixes (e.g., <em>gene</em>) or other rules
discussed in the project’s
<a href="https://github.com/biopragmatics/bioregistry/blob/main/docs/CONTRIBUTING.md">contribution guidelines</a>.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="Bioregistry" /><category term="prefixes" /><category term="W3C" /><category term="Python" /><summary type="html"><![CDATA[This post gives a brief background on the formal definition of the syntax and semantics of compact uniform resource identifiers (CURIEs) from the Worldwide Web Consortium (W3C) and investigates how many prefixes in the Bioregistry are compliant with the standard.]]></summary></entry><entry><title type="html">Idiomatic conversion between URIs and compact URIs</title><link href="https://cthoyt.com/2023/01/10/curies-package.html" rel="alternate" type="text/html" title="Idiomatic conversion between URIs and compact URIs" /><published>2023-01-10T12:28:00+00:00</published><updated>2023-01-10T12:28:00+00:00</updated><id>https://cthoyt.com/2023/01/10/curies-package</id><content type="html" xml:base="https://cthoyt.com/2023/01/10/curies-package.html"><![CDATA[<p>The semantic web and ontology communities needed a reusable Python package for
converting between uniform resource identifiers (URIs) and compact URIs (CURIEs)
that is reliable, idiomatic, generic, and performant. This post describes the
<a href="https://github.com/cthoyt/curies"><code class="language-plaintext highlighter-rouge">curies</code></a> Python package that fills this
need.</p>

<p>After installing with <code class="language-plaintext highlighter-rouge">pip install curies</code> or checking out the code on
<a href="https://github.com/cthoyt/curies">GitHub</a> and installing a local copy, you can
directly jump in to using the <code class="language-plaintext highlighter-rouge">curies</code> package. Its main data structure is
<a href="https://curies.readthedocs.io/en/latest/api/curies.Converter.html#converter"><code class="language-plaintext highlighter-rouge">curies.Converter</code></a>.
It can be instantiated with various class methods corresponding to data in one
of several formats.</p>

<p>The most common format is a prefix map, a dictionary containing a one-to-many
mapping from CURIE prefixes to URI prefixes. It can be used in combination with
the
<a href="https://curies.readthedocs.io/en/latest/api/curies.Converter.html#curies.Converter.from_prefix_map"><code class="language-plaintext highlighter-rouge">Converter.from_prefix_map</code></a>
class method. The following example includes some (but not all) of the CURIE and
URI prefixes used by ontologies in the
<a href="https://obofoundry.org">Open Biological and Biomedical Ontology (OBO) Foundry</a>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">curies</span> <span class="kn">import</span> <span class="n">Converter</span>

<span class="n">prefix_map</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">"CHEBI"</span><span class="p">:</span> <span class="s">"http://purl.obolibrary.org/obo/CHEBI_"</span><span class="p">,</span>
    <span class="s">"MONDO"</span><span class="p">:</span> <span class="s">"http://purl.obolibrary.org/obo/MONDO_"</span><span class="p">,</span>
    <span class="s">"GO"</span><span class="p">:</span> <span class="s">"http://purl.obolibrary.org/obo/GO_"</span><span class="p">,</span>
    <span class="c1"># ... and so on
</span>    <span class="s">"OBO"</span><span class="p">:</span> <span class="s">"http://purl.obolibrary.org/obo/"</span><span class="p">,</span>
<span class="p">}</span>
<span class="n">converter</span> <span class="o">=</span> <span class="n">Converter</span><span class="p">.</span><span class="n">from_prefix_map</span><span class="p">(</span><span class="n">prefix_map</span><span class="p">)</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Converter</code> class indexes the prefix map using a
<a href="https://en.wikipedia.org/wiki/Trie">trie</a> data structure, which makes search of
the beginning of sequences (such as strings) efficient. The <code class="language-plaintext highlighter-rouge">curies</code>
implementation builds on the implementation of this data structure in the
<a href="https://github.com/gsakkis/pytrie/"><code class="language-plaintext highlighter-rouge">PyTrie</code></a> package.</p>

<h2 id="conversion">Conversion</h2>

<p>A uniform resource identifier (URI) that corresponds to one of the URI prefixes
registered in the converter can be compressed into a compact URI (CURIE) using
the
<a href="https://curies.readthedocs.io/en/latest/api/curies.Converter.html#curies.Converter.compress"><code class="language-plaintext highlighter-rouge">Converter.compress</code></a>
method. In the following example, we use the canonical URI (within the scope of
the OBO Foundry) for the <a href="http://geneontology.org/">Gene Ontology</a> term for
<a href="http://purl.obolibrary.org/obo/GO_0032571">response to vitamin K (GO:0032571)</a>.</p>

<pre><code class="language-python-repl">&gt;&gt;&gt; converter.compress("http://purl.obolibrary.org/obo/GO_0032571")
'GO:0032571'
</code></pre>

<p>When some URI prefixes are partially overlapping (e.g.,
<code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/CHEBI_</code> for <code class="language-plaintext highlighter-rouge">GO</code> and
<code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/</code> for <code class="language-plaintext highlighter-rouge">OBO</code>), the longest URI prefix will always
be matched. For example, compressing <code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/GO_0032571</code>
returns <code class="language-plaintext highlighter-rouge">GO:0032571</code> instead of <code class="language-plaintext highlighter-rouge">OBO:GO_0032571</code>.</p>

<p>If there’s no matching URI prefix, then <code class="language-plaintext highlighter-rouge">compress()</code> will return <code class="language-plaintext highlighter-rouge">None</code>.</p>

<pre><code class="language-python-repl">&gt;&gt;&gt; converter.compress("http://example.com/missing:0000000") is None
True
</code></pre>

<p>Similarly, a CURIE can be expanded into a URI using the
<a href="https://curies.readthedocs.io/en/latest/api/curies.Converter.html#curies.Converter.expand"><code class="language-plaintext highlighter-rouge">Converter.expand</code></a>
method.</p>

<pre><code class="language-python-repl">&gt;&gt;&gt; converter.expand("GO:0032571")
'http://purl.obolibrary.org/obo/GO_0032571'
</code></pre>

<p>If there’s no matching CURIE prefix, then <code class="language-plaintext highlighter-rouge">expand()</code> will return <code class="language-plaintext highlighter-rouge">None</code>.</p>

<pre><code class="language-python-repl">&gt;&gt;&gt; converter.expand("missing:0000000") is None
True
</code></pre>

<h2 id="getting-prefix-maps">Getting Prefix Maps</h2>

<p>The <code class="language-plaintext highlighter-rouge">curies</code> package includes functions for loading several prefix maps from
external resources. These are not cached in order to take advantage of the most
recent versions. This is particularly important for resources like the
<a href="https://bioregistry.io">Bioregistry</a> that are updated frequently.</p>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Function</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Bioregistry</td>
      <td><a href="https://curies.readthedocs.io/en/latest/api/curies.get_bioregistry_converter.html"><code class="language-plaintext highlighter-rouge">curies.get_bioregistry_converter</code></a></td>
      <td>A high-coverage, general purpose registry for the life and natural sciences.</td>
    </tr>
    <tr>
      <td>OBO Foundry</td>
      <td><a href="https://curies.readthedocs.io/en/latest/api/curies.get_obo_converter.html"><code class="language-plaintext highlighter-rouge">curies.get_obo_converter</code></a></td>
      <td>A set of orthogonal ontologies for the life sciences constructed for mutual interoperability</td>
    </tr>
    <tr>
      <td>Prefix Commons</td>
      <td><a href="https://curies.readthedocs.io/en/latest/api/curies.get_prefixcommons_converter.html"><code class="language-plaintext highlighter-rouge">curies.get_prefixcommons_converter</code></a></td>
      <td>A medium-coverage, general purpose registry for the life and natural sciences</td>
    </tr>
    <tr>
      <td>Gene Ontology</td>
      <td><a href="https://curies.readthedocs.io/en/latest/api/curies.get_go_converter.html"><code class="language-plaintext highlighter-rouge">curies.get_go_converter</code></a></td>
      <td>A project-specific prefix map for the Gene Ontology, includes several duplicate and non-standard definitions</td>
    </tr>
    <tr>
      <td>Monarch</td>
      <td><a href="https://curies.readthedocs.io/en/latest/api/curies.get_monatch_converter.html"><code class="language-plaintext highlighter-rouge">curies.get_monarch_converter</code></a></td>
      <td>A project-specific prefix map for the Monarch Initiative, includes several duplicate and non-standard definitions</td>
    </tr>
  </tbody>
</table>

<h3 id="loading-from-the-bioregistry">Loading from the Bioregistry</h3>

<p>The <a href="https://github.com/biopragmatics/bioregistry"><code class="language-plaintext highlighter-rouge">bioregistry</code></a> Python package
has first-class support for the <code class="language-plaintext highlighter-rouge">curies</code> package through the generic function
<a href="https://bioregistry.readthedocs.io/en/stable/api/bioregistry.get_converter.html"><code class="language-plaintext highlighter-rouge">bioregistry.get_converter</code></a>.
This can be used as an alternative to <code class="language-plaintext highlighter-rouge">curies.get_bioregistry_converter</code> in
cases when the Bioregistry is installed and it’s desired to use local data.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">bioregistry</span>
<span class="kn">from</span> <span class="nn">curies</span> <span class="kn">import</span> <span class="n">Converter</span>

<span class="n">converter</span><span class="p">:</span> <span class="n">Converter</span> <span class="o">=</span> <span class="n">bioregistry</span><span class="p">.</span><span class="n">get_converter</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="loading-from-prefixmaps">Loading from <code class="language-plaintext highlighter-rouge">prefixmaps</code></h3>

<p>The <a href="https://github.com/linkml/prefixmaps"><code class="language-plaintext highlighter-rouge">prefixmaps</code></a> Python package keeps
various prefix maps under version control that also has partial support for the
<code class="language-plaintext highlighter-rouge">curies</code> package using the <em>extended prefix map</em> data structure (as opposed to a
prefix map, this includes synonyms). See
<a href="https://curies.readthedocs.io/en/latest/api/curies.Converter.html#curies.Converter.from_extended_prefix_map"><code class="language-plaintext highlighter-rouge">Converter.from_extended_prefix_map</code></a>
for more information on how to use this data structure.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">prefixmaps</span> <span class="kn">import</span> <span class="n">load_context</span>
<span class="kn">from</span> <span class="nn">curies</span> <span class="kn">import</span> <span class="n">Converter</span>

<span class="n">extended_prefix_map</span> <span class="o">=</span> <span class="n">load_context</span><span class="p">(</span><span class="s">"obo"</span><span class="p">).</span><span class="n">as_extended_prefix_map</span><span class="p">()</span>
<span class="n">converter</span> <span class="o">=</span> <span class="n">Converter</span><span class="p">.</span><span class="n">from_extended_prefix_map</span><span class="p">(</span><span class="n">extended_prefix_map</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="related">Related</h2>

<p>Here’s a short (probably incomplete) list of other packages I’ve found that have
related functionalities:</p>

<ul>
  <li>https://github.com/prefixcommons/prefixcommons-py (Python)</li>
  <li>https://github.com/prefixcommons/curie-util (Java)</li>
  <li>https://github.com/geneontology/curie-util-py (Python)</li>
  <li>https://github.com/geneontology/curie-util-es5 (Node.js)</li>
  <li>https://github.com/endoli/curie.rs (Rust)</li>
</ul>

<hr />

<p>This post didn’t touch the more advanced features of the <code class="language-plaintext highlighter-rouge">Converter</code> class such
as its support for CURIE prefix synonyms and URI prefix synonyms. It also didn’t
touch the <code class="language-plaintext highlighter-rouge">curies.chain()</code> function which enables several pre-instantiated
converters to be used in succession, similarly to the Python built-in
<code class="language-plaintext highlighter-rouge">collections.ChainMap</code> class. These are described in the documentation at
<a href="https://curies.readthedocs.io">curies.readthedocs.io</a></p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="semantic web" /><category term="CURIEs" /><category term="prefixes" /><category term="IRIs" /><category term="URIs" /><category term="Python" /><summary type="html"><![CDATA[The semantic web and ontology communities needed a reusable Python package for converting between uniform resource identifiers (URIs) and compact URIs (CURIEs) that is reliable, idiomatic, generic, and performant. This post describes the curies Python package that fills this need.]]></summary></entry><entry><title type="html">Long-term Funding for Small Biomedical Databases</title><link href="https://cthoyt.com/2023/01/04/small-website-grant-idea.html" rel="alternate" type="text/html" title="Long-term Funding for Small Biomedical Databases" /><published>2023-01-04T15:32:00+00:00</published><updated>2023-01-04T15:32:00+00:00</updated><id>https://cthoyt.com/2023/01/04/small-website-grant-idea</id><content type="html" xml:base="https://cthoyt.com/2023/01/04/small-website-grant-idea.html"><![CDATA[<p>Way back in 2021, during the annual general assembly of the
<a href="https://www.biocuration.org">International Society for Biocuration (ISB)</a> at
the
<a href="https://www.biocuration.org/14th-annual-biocuration-conference-virtual/">14th Annual International Biocuration Conference (Biocuration 2021)</a>
, there was a discussion about the notably underutilized budget of the society
that resulted in an informal open call for ideas for new small funding schemes.
Concurrently, discussions with external stakeholders for the relatively new (at
the time) <a href="https://bioregistry.io">Bioregistry</a> project often included questions
about the sustainability and longevity of the resource. We had conservatively
estimated it would cost about 100 USD/year to run the Bioregistry site, so this
seemed like the perfect opportunity to ask for a small amount funding
distributed over a relatively long period of time. This post is about the more
general reality of funding for small resources in the life sciences, how we
petitioned the ISB for funding, and what happened next.</p>

<h2 id="bioregistry-cost-estimate">Bioregistry Cost Estimate</h2>

<p>The Bioregistry is an integrative registry of biomedical ontologies, databases,
and other identifier resources that is useful for standardizing the
identification of biomedical concepts, making data more FAIR, and enabling data-
and knowledge integration.</p>

<p>It runs a fully automated nightly
<a href="https://github.com/biopragmatics/bioregistry/actions/workflows/update.yml">workflow</a>
that updates its data, packages its code and data, and pushes it to the Python
Package Index (PyPI). We carefully constructed this workflow to fully run on
free, open infrastructure (e.g., GitHub Actions) and require little maintenance.
After running this workflow for nearly three years, we’ve empirically found that
the maintenance effort averages between 10-15 minutes per month for bug fixes
and/or additional data curation. We won’t factor this into our initial cost
estimate as this effort could be discretionary (i.e., not tied to a grant,
donated by maintainers, users, etc.).</p>

<p>After, the workflow builds a Docker image and pushes it to the
<a href="https://hub.docker.com/r/biopragmatics/bioregistry">biopragmatics/bioregistry</a>
repository on <a href="https://hub.docker.com">DockerHub</a>. This image is built with the
the latest Python alpine base image, which significantly reduces non-essential
components. It also has the benefit of being compatible with Docker environments
running on both Intel and ARM architectures. The final compressed image weights
less than 40 MB of disk space and runs inside Docker with about 65 MB of memory
at baseline. This easily fits on a dedicated
<a href="https://aws.amazon.com/ec2/instance-types/t4/">t4g.nano</a> instance on Amazon Web
Services (AWS) which costs about 37 USD/year on-demand or around 20 USD/year
reserved (i.e., if you pay up front). Two or more of these instances could be
set up behind a <a href="https://aws.amazon.com/what-is/load-balancing/">load balancer</a>
to ensure uptime in case one goes down, but we won’t factor this into our
initial cost estimate.</p>

<p>The <code class="language-plaintext highlighter-rouge">bioregistry.io</code> domain is registered with
<a href="https://www.namecheap.com">Namecheap</a> and costs about 33 USD/year. The SSL/TLS
certificate for <code class="language-plaintext highlighter-rouge">bioregistry.io</code> (so it can be served with HTTPS) is managed
through the
<a href="https://aws.amazon.com/certificate-manager/">AWS Certificate Manager</a> and does
not cost anything additional.</p>

<p>Finally, we’re going to exclude the costs of proactive curation of the resource.
The combination of import from 23+ external registries, the ability for
community members to suggest new prefixes in a streamlined way, and the
decentralization of moderation effort across many institutions results in a
negligible cost.</p>

<p>In a best case scenario, using a reserved instance and paying for a domain name
brings this bill to 53 USD/year. Duplicating the deployment behind a load
balancer would bring it up to 73 USD/year. For good measures in case costs go
up, let’s call it 100 USD/year. That’s pretty darn cheap for a resource that’s
already proven useful in several applications and is being pretty widely
adopted. Most resources don’t last more than two or three years (disclaimer: I’m
spit-balling here). If we got 1,000 USD, we could already promise people that
this service will be running consistently for ten years. If we got 1,500 USD, we
could extend that promise to fifteen years.</p>

<p>In order to gauge how small this amount is, consider that the International
Society of Biocuration is currently offering
<a href="https://www.biocuration.org/travel-fellowship/">six travel grants</a> worth about
the same amount each to attend the
<a href="https://biocuration2023.github.io/">16th Annual International Biocuration Conference (Biocuration 2023)</a>.
Sure, I’m biased since the Bioregistry is my baby, but it would be difficult to
argue that this is not pennies on the dollar compared to what some other
resources cost to deploy and maintain.</p>

<h2 id="whats-the-problem-with-funding-the-bioregistry">What’s the Problem with Funding the Bioregistry</h2>

<p>The Bioregistry was a spin-off project of a spin-off project. A long time ago, I
was working on parsing and validating biomedical knowledge encoded in the
<a href="https://biological-expression-language.github.io/">Biological Expression Language</a>.
In order to support the validation of names and identifiers from various
ontologies and databases, I started developing
<a href="https://github.com/pyobo/pyobo">PyOBO</a> as a tool for unified access to
ontologies and databases. I curated extra rules for standardizing prefixes,
CURIEs, and URIs that eventually got spun out into the Bioregistry and took on a
life of its own. Each of these steps was part of a bigger project with bigger
goals that weren’t necessarily focused on the development of high-quality,
reusable underlying technology. But, developing high-quality, reusable
underlying technology is just the way that I think, so that’s how I did it. And
I took those nice tools with me from project to project, improving them as I
went. This means that the Bioregistry has now played a role in several of my
recent projects, but never sat up front and center when it came to writing
grants.</p>

<p>So here’s the more general issue: grants are usually about building new, big
things with big budgets that are done within the short timeframe of a project,
then they’re usually lost or forgotten. The Bioregistry is a small, important
resource that does not need massive development but it does need a long
commitment to maintenance in order to provide the stability to the community
that other resources have yet been able to provide. Further, the fact that it
already exists makes it much less novel and therefore effectively disqualified
from being the focus of most potential grants.</p>

<p>With potential costs so low, it would actually feasible for my current
department, the <a href="https://labsyspharm.org">Laboratory of Systems Pharmacology</a> to
commit discretionary funding external to a grant. However, this doesn’t engender
trust the same as having external funding or being part of an institution that
is well-known for the longevity of its infrastructure platforms (e.g., the NIH).
Further, discretionary funding also raises questions about what happens when the
main people on my team working on the Bioregistry leave, both from a
responsibility and technical perspective. This seemed to be the sentiment from
potential stakeholders of the Bioregistry that were shared at the
<a href="https://biopragmatics.github.io/workshops/WPCI2021.html">Fall 2021 Workshop on Prefixes, CURIEs, and IRIs</a>.</p>

<h2 id="communication-with-the-isb">Communication with the ISB</h2>

<p>After thinking about all of this, I finally write up a proposal (somewhere
between one and two pages) and shared it with the executive board of the ISB.
Unfortunately, I shared this on Slack and have been unable to find the original
message since you can’t search Slack messages older than 90 days now. As a
paraphrase, I focused on some of the following ideas:</p>

<p>As opposed to monolithic infrastructures and projects maintained by large
organizations like the
<a href="https://www.nlm.nih.gov">United States National Library of Medicines (NLM)</a>,
the <a href="https://www.ebi.ac.uk">European Bioinformatics Institute (EMBL-EBI)</a>, and
the <a href="https://www.sib.swiss">Swiss Institute of Bioinformatics</a>, most biocuration
projects are small and limited in scope. There are several public systems that
could support many of them:</p>

<ul>
  <li>archival systems (e.g., <a href="https://zenodo.org">Zenodo</a>,
<a href="https://figshare.com">FigShare</a>)</li>
  <li>hosting of static sites (e.g., institutional websites,
<a href="https://pages.github.com/">GitHub Pages</a>)</li>
  <li>hosting of dynamic sites (e.g.,
<a href="https://aws.amazon.com/">Amazon Web Services</a>,
<a href="https://azure.microsoft.com/en-us/">Microsoft Azure</a>,
<a href="https://www.heroku.com/">Heroku</a>, <a href="https://www.shinyapps.io/">Shiny</a>,
<a href="https://cloud.google.com/">Google Could Platform</a>)</li>
  <li>domain name registration (e.g., institutional domain,
<a href="https://www.namecheap.com/">NameCheap</a>)</li>
  <li>continuous integration and deployment (e.g., GitHub Actions, GitLab Pipelines)</li>
</ul>

<p>These are relatively inexpensive and can be used to build resources that have
high longevity and sustainability. Most biocuration projects end up being
one-off publications anyway and are never updated, so I suggested that the ISB
give small grants to groups that are using best practices and the above tools to
create things that could have longevity, if given appropriate small funding
across a long period of time. The biggest problem facing small curation projects
and websites is that they can not acquire small amounts of long-term funding to
support their technical infrastructure (e.g., domain name hosting, cloud compute
resources). It’s becoming easier to build websites for new small biocuration
projects, but again, there are small, recurring costs associated with them like
paying for a domain name and cloud compute resources. I would suggest
introducing a small grant whose award would be spread across a potentially long
time (e.g., 10-15 years) to support those costs (but not explicitly not for
major development nor maintenance).</p>

<h2 id="is-this-really-a-big-deal">Is this really a big deal?</h2>

<p>My group has recently been thinking about how bad the longevity situation really
is. We build the
<a href="https://biopragmatics.github.io/bioregistry/health/">Bioregistry Health Report</a>
that checks on a weekly basis how many prefixes’ homepages still resolve and how
many URI format strings still resolve. The results were bleak - only about 27%
(as of today) do. This isn’t an indictment of the badness of the Bioregistry or
the registries that it imports and aligns either - it’s true that sometimes the
URLs for sites change, but this number reflects the more general problem that
most just go away.</p>

<h2 id="the-results">The Results</h2>

<p>Unfortunately, the ISB EC didn’t accept the proposal. I didn’t get very direct
feedback as to why, but I think the general concept of funding research was
problematic for one reason or another for their organizational structure. I’m
not discouraged, I still think this is important and will continue to write and
re-write this proposal until it finally convinces the right funding body. I’ll
also be touching on this point at my (~hopefully~ accepted abstract) for
Biocuration 2023 that I <a href="/2023/01/03/biocuration2023-abstract.html">posted about
yesterday</a>. Please let me
know if you’ve got any other arguments supporting doing this that I can include!</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="funding" /><category term="longevity" /><category term="sustainability" /><summary type="html"><![CDATA[Way back in 2021, during the annual general assembly of the International Society for Biocuration (ISB) at the 14th Annual International Biocuration Conference (Biocuration 2021) , there was a discussion about the notably underutilized budget of the society that resulted in an informal open call for ideas for new small funding schemes. Concurrently, discussions with external stakeholders for the relatively new (at the time) Bioregistry project often included questions about the sustainability and longevity of the resource. We had conservatively estimated it would cost about 100 USD/year to run the Bioregistry site, so this seemed like the perfect opportunity to ask for a small amount funding distributed over a relatively long period of time. This post is about the more general reality of funding for small resources in the life sciences, how we petitioned the ISB for funding, and what happened next.]]></summary></entry><entry><title type="html">Promoting the longevity of curated scientific resources through open code, open data, and public infrastructure</title><link href="https://cthoyt.com/2023/01/03/biocuration2023-abstract.html" rel="alternate" type="text/html" title="Promoting the longevity of curated scientific resources through open code, open data, and public infrastructure" /><published>2023-01-03T16:28:00+00:00</published><updated>2023-01-03T16:28:00+00:00</updated><id>https://cthoyt.com/2023/01/03/biocuration2023-abstract</id><content type="html" xml:base="https://cthoyt.com/2023/01/03/biocuration2023-abstract.html"><![CDATA[<p>The
<a href="https://biocuration2023.github.io">16th Annual International Biocuration Conference (Biocuration 2023)</a>
is taking place in Padua, Italy from April 24-26<sup>th</sup>, 2023. While I’m
serving as a co-chair of the conference, I also think this is a great venue to
communicate some of my thoughts on longevity and sustainability that have been
gestating during the development of the <a href="https://bioregistry.io">Bioregistry</a>
and other <a href="https://biopragmatics.github.io/">Biopragmatics</a> projects. This blog
post contains the abstract I’ve submitted for oral presentation.</p>

<p><strong>Note</strong>: if you want to submit your own abstract, you have until the end of
today (January 3<sup>rd</sup>, 2023, anywhere on earth) to submit via
<a href="https://easychair.org/conferences/?conf=biocuration2023">EasyChair</a>.</p>

<h2 id="abstract">Abstract</h2>

<p>Many model organism databases, pathway databases, ontologies, and other curated
resources that support research in the life and natural sciences combine
expert-curated data with surrounding software code and services. However, such
resources are often maintained internally by members of a single institution and
are therefore susceptible to fluctuations in funding, personnel, and
institutional priorities. Too often, resources go out of date, are abandoned, or
become inaccessible, for example, when a grant runs out or a key person moves
on. Therefore, we need better solutions for creating resources that are less
susceptible to such external factors and can continue to be used and maintained
by the community that they serve.</p>

<p>We propose a new model for the creation and maintenance of curated resources
that promotes longevity through a combination of technical and social workflows,
and a progressive governance model that supports and encourages community-driven
curation. <strong>1)</strong> The technical aspect of our model necessitates open data, open
code, and open infrastructure. Both code and data are permissively licensed and
kept together under public version control. This enables anyone to directly
suggest improvements and updates. Further, automation is used for continuous
integration (e.g., semi-automated curation, quality assurance) and continuous
delivery (e.g., static website generation, export in multiple formats). <strong>2)</strong>
The social aspect of our model first prescribes the composition of training
material, curation guidelines, contribution guidelines, and a community code of
conduct that encourage and support potential community curators. Second, it
requires the use of public tools for suggestions, questions, discussion as well
as social workflows like pull requests for the submission and review of changes.
<strong>3)</strong> The governance aspect of our model necessitates the division of
responsibilities and authority (e.g., for reviewing/merging changes to the
code/data) across multiple institutions such that it is more robust to
fluctuation in funding and personnel that can also be updated over time. It
prescribes liberal attribution and acknowledgement of the individuals and
institutions (both internal and external to the project) who contribute on a
variety of levels (e.g., code, data, discussion, funding). More generally, our
model requires that a minimal governance model is codified and instituted as
early as possible in a project’s lifetime.</p>

<p>This talk will provide a perspective on how existing resources relate to our
model, describe each of our model’s aspects in more detail (illustrated through
the <a href="https://bioregistry.io">Bioregistry</a> resource), and provide a practical
path towards both creating new sustainable resources as well as revitalizing
existing ones.</p>

<h2 id="recording">Recording</h2>

<p>My talk was accepted! You can check the
<a href="https://docs.google.com/presentation/d/1hEryoMNyp8gwt2pvPy2ErKaw_QPvnWOmzPT0Sn4Yl4s/edit?usp=sharing">slides</a>
or watch the video recording:</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/kuJsl-rRjZY?si=US80bj-1FOHuA5Yr" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen=""></iframe>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="biocuration" /><category term="philosophy" /><summary type="html"><![CDATA[The 16th Annual International Biocuration Conference (Biocuration 2023) is taking place in Padua, Italy from April 24-26th, 2023. While I’m serving as a co-chair of the conference, I also think this is a great venue to communicate some of my thoughts on longevity and sustainability that have been gestating during the development of the Bioregistry and other Biopragmatics projects. This blog post contains the abstract I’ve submitted for oral presentation.]]></summary></entry><entry><title type="html">Connecting Preprints to Peer-reviewed Articles on Wikidata</title><link href="https://cthoyt.com/2023/01/02/wikidata-preprints.html" rel="alternate" type="text/html" title="Connecting Preprints to Peer-reviewed Articles on Wikidata" /><published>2023-01-02T18:27:00+00:00</published><updated>2023-01-02T18:27:00+00:00</updated><id>https://cthoyt.com/2023/01/02/wikidata-preprints</id><content type="html" xml:base="https://cthoyt.com/2023/01/02/wikidata-preprints.html"><![CDATA[<p>After the <a href="https://arxiv.org/abs/2212.13543">BioCypher</a> preprint went up on the
<em>arXiv</em>, I checked in on the
<a href="https://scholia.toolforge.org/author/Q47475003/curation#missing-coauthor-items">missing co-author items</a>
list on the Scholia page that reflects
<a href="https://www.wikidata.org/wiki/Q47475003">my Wikidata entry</a>. In addition to the
several co-authors of the BioCypher manuscript that I don’t know personally, I
was curious to see which other papers of mine did not have fully complete
co-author annotations. This post has a few SPARQL queries that I used to look
into this as well as a few ongoing questions I have about the relationship
between distinct entries for preprints and published articles.</p>

<p>First, I wrote two SPARQL queries for the
<a href="https://query.wikidata.org">Wikidata Query Service</a>:</p>

<ul>
  <li><a href="https://w.wiki/6BNF">co-authors of mine that are disambiguated</a></li>
  <li><a href="https://w.wiki/6BNB">co-authors of mine that are <em>not</em> disambiguated</a></li>
</ul>

<p>There were around 200 co-authors that I had included through a painstaking
combination of manual curation and usage of the
<a href="https://author-disambiguator.toolforge.org">Author Disambiguator</a> tool.
However, when I looked at the ambiguous authors, i.e., authors that only stored
by name via <em>author name string</em>
(<a href="https://www.wikidata.org/wiki/Property:P2093">P2093</a>) instead of by reference
to a Wikidata entry via <em>author</em>
(<a href="https://www.wikidata.org/wiki/Property:P50">P50</a>). At the time, this included
co-authors from only four unique manuscripts including:</p>

<ol>
  <li><a href="http://www.wikidata.org/entity/Q115929717">Democratising Knowledge Representation with BioCypher</a></li>
  <li><a href="http://www.wikidata.org/entity/Q114032935">A Simple Standard for Sharing Ontological Mappings (SSSOM)</a>,</li>
  <li><a href="http://www.wikidata.org/entity/Q114032921">Leveraging Structured Biological Knowledge for Counterfactual Inference: A Case Study of Viral Pathogenesis</a></li>
  <li><a href="http://www.wikidata.org/entity/Q112942500">Ontology Development Kit: a toolkit for building, maintaining, and standardising biomedical ontologies</a></li>
</ol>

<p>The appearance of the BioCypher manuscript on this list was no surprising
because we just preprinted it on <em>arXiv</em>. However, I very clearly remember
carefully curating all the co-authors of the other papers. After more careful
inspection, it turns out that I had indeed done this curation for the preprints
of each of these articles, but the ones appearing on the list corresponded to
duplicate Wikidata entries not for the preprints, but for the published papers.
This led me to a couple questions, which I don’t have answers for yet:</p>

<ol>
  <li>Should there two different entries for a preprint and a publication?</li>
  <li>What’s even the right word for the dichotomy between a preprint and a
publication? I don’t think it’s post-print.</li>
  <li>It appears that there are multiple different entries between preprints and
publications, so question 1) is a “perfect world” question. Since, in
reality, there are duplicates, how should we handle them?
    <ul>
      <li>Should we connect them via some kind of relationship? I recently noticed
Tiago Lubiana had been using <em>followed by</em>
(<a href="https://www.wikidata.org/wiki/Property:P156">P156</a>) for papers he had
curated and have started using that myself in some cases (see notes below).</li>
      <li>Should we merge these two entries into one? There are various properties
for the identifiers within preprint servers that can help point to
pre-prints, though preprints are given different DOIs than the publication
so maybe this would create confusion.</li>
      <li>How will this work with the advent of “overlay journals”, like what <em>eLife</em>
is doing by more heavily relying on peer review attached to existing
preprints?</li>
    </ul>
  </li>
  <li>To what extent does this confusion affect Wikidata content related to me
(e.g., my papers)?</li>
</ol>

<p>In order to assess my own Wikidata cleanliness, I wrote the following SPARQL
query:</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="nv">?preprint</span><span class="w"> </span><span class="nv">?preprintDate</span><span class="w"> </span><span class="nv">?followedBy</span><span class="w"> </span><span class="nv">?article</span><span class="w"> </span><span class="nv">?articleDate</span><span class="w"> </span><span class="nv">?label</span><span class="w">
</span><span class="k">WHERE</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="k">VALUES</span><span class="w"> </span><span class="nv">?author</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q47475003</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="nv">?preprint</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P31</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q580922</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P50</span><span class="w"> </span><span class="nv">?author</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">rdfs</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="nv">?preprintLabel</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="nv">?article</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P31</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q13442814</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P50</span><span class="w"> </span><span class="nv">?author</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">rdfs</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="nv">?label</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">OPTIONAL</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">?preprint</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P577</span><span class="w"> </span><span class="nv">?preprintDate</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="k">OPTIONAL</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">?article</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P577</span><span class="w"> </span><span class="nv">?articleDate</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="k">OPTIONAL</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">?preprint</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P156</span><span class="w"> </span><span class="nv">?followedBy</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="k">FILTER</span><span class="w"> </span><span class="p">(</span><span class="nb">LCASE</span><span class="p">(</span><span class="nv">?preprintLabel</span><span class="p">)</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">LCASE</span><span class="p">(</span><span class="nv">?label</span><span class="p">))</span><span class="w">
  </span><span class="k">FILTER</span><span class="w"> </span><span class="p">(</span><span class="nv">?preprint</span><span class="w"> </span><span class="o">!</span><span class="p">=</span><span class="w"> </span><span class="nv">?article</span><span class="p">)</span><span class="w">
  </span><span class="k">FILTER</span><span class="w"> </span><span class="p">(</span><span class="nb">LANG</span><span class="p">(</span><span class="nv">?preprintLabel</span><span class="p">)</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s2">"en"</span><span class="p">)</span><span class="w">
  </span><span class="k">FILTER</span><span class="w"> </span><span class="p">(</span><span class="nb">LANG</span><span class="p">(</span><span class="nv">?label</span><span class="p">)</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s2">"en"</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">DESC</span><span class="p">(</span><span class="nv">?articleDate</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>Here are the live results from running that SPARQL query, embedded via the
Wikidata Query Service:</p>

<iframe style="width: 80vw; height: 50vh; border: none;" src="https://query.wikidata.org/embed.html#%0ASELECT%20%3Fpreprint%20%3FfollowedBy%20%3Farticle%20%3Flabel%0AWHERE%20%0A%7B%0A%20%20VALUES%20%3Fauthor%20%7B%20wd%3AQ47475003%20%7D%0A%20%20%3Fpreprint%20wdt%3AP31%20wd%3AQ580922%20%3B%0A%20%20%20%20wdt%3AP50%20%3Fauthor%20%3B%0A%20%20%20%20rdfs%3Alabel%20%3FpreprintLabel%20.%0A%20%20%3Farticle%20wdt%3AP31%20wd%3AQ13442814%20%3B%0A%20%20%20%20wdt%3AP50%20%3Fauthor%20%3B%0A%20%20%20%20rdfs%3Alabel%20%3Flabel%20.%0A%20%20OPTIONAL%20%7B%20%3Fpreprint%20wdt%3AP156%20%3FfollowedBy%20%7D%0A%20%20FILTER%20%28LCASE%28%3FpreprintLabel%29%20%3D%20LCASE%28%3Flabel%29%29%0A%20%20FILTER%20%28%3Fpreprint%20%21%3D%20%3Farticle%29%0A%20%20FILTER%20%28LANG%28%3FpreprintLabel%29%20%3D%20%22en%22%29%0A%20%20FILTER%20%28LANG%28%3Flabel%29%20%3D%20%22en%22%29%0A%7D%0AORDER%20BY%20DESC%28%3FarticleDate%29" referrerpolicy="origin" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>

<p>At the time of writing, there are 15 duplicates (based on case insensitive
string matching). I’ve begun curating the followed-by relationships, but am
holding out since I might be able to come up with a script to automatically
generate appropriate
<a href="https://quickstatements.toolforge.org/#/">quickstatements</a>.</p>

<p>Interestingly, the fact that these are different entries allows an alternate
view that gives insight in turnover from preprint date to publication date.
Considering that I typically preprint the paper and send for peer review
simultaneously, this is an interesting statistic.</p>

<iframe style="width: 80vw; height: 50vh; border: none;" src="https://query.wikidata.org/embed.html#%23defaultView%3ATimeline%0ASELECT%20%3Fpreprint%20%3FpreprintDate%20%3Farticle%20%3FarticleDate%20%3Flabel%0AWHERE%20%0A%7B%0A%20%20VALUES%20%3Fauthor%20%7B%20wd%3AQ47475003%20%7D%0A%20%20%3Fpreprint%20wdt%3AP31%20wd%3AQ580922%20%3B%0A%20%20%20%20wdt%3AP50%20%3Fauthor%20%3B%0A%20%20%20%20rdfs%3Alabel%20%3FpreprintLabel%20.%0A%20%20%3Farticle%20wdt%3AP31%20wd%3AQ13442814%20%3B%0A%20%20%20%20wdt%3AP50%20%3Fauthor%20%3B%0A%20%20%20%20rdfs%3Alabel%20%3Flabel%20.%0A%20%20OPTIONAL%20%7B%20%3Fpreprint%20wdt%3AP577%20%3FpreprintDate%20%7D%0A%20%20OPTIONAL%20%7B%20%3Farticle%20wdt%3AP577%20%3FarticleDate%20%7D%0A%20%20FILTER%20%28LCASE%28%3FpreprintLabel%29%20%3D%20LCASE%28%3Flabel%29%29%0A%20%20FILTER%20%28%3Fpreprint%20%21%3D%20%3Farticle%29%0A%20%20FILTER%20%28LANG%28%3FpreprintLabel%29%20%3D%20%22en%22%29%0A%20%20FILTER%20%28LANG%28%3Flabel%29%20%3D%20%22en%22%29%0A%7D%0AORDER%20BY%20DESC%28%3FarticleDate%29%0A" referrerpolicy="origin" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>

<hr />

<p>Usually I try and write blog posts about something I made or some insight that I
got out of working on something, but I’m not really sure where to go from here.
Ideally, I’d like to see the entirety of PubMed, PMC, other major scholarly
article indexes, arXiv, bioRxiv, other preprint servers, and other bibliographic
content automatically aligned in full on Wikidata. I’ve heard that there are
concerns about the technical limitations about the service so this might not be
feasible in the near future. In the mean time, if you’ve got some answers for my
questions, please let me know.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="bibliometrics" /><summary type="html"><![CDATA[After the BioCypher preprint went up on the arXiv, I checked in on the missing co-author items list on the Scholia page that reflects my Wikidata entry. In addition to the several co-authors of the BioCypher manuscript that I don’t know personally, I was curious to see which other papers of mine did not have fully complete co-author annotations. This post has a few SPARQL queries that I used to look into this as well as a few ongoing questions I have about the relationship between distinct entries for preprints and published articles.]]></summary></entry><entry><title type="html">Global Core Biodata Resources in the Bioregistry</title><link href="https://cthoyt.com/2022/12/19/gcbr-in-bioregistry.html" rel="alternate" type="text/html" title="Global Core Biodata Resources in the Bioregistry" /><published>2022-12-19T12:14:00+00:00</published><updated>2022-12-19T12:14:00+00:00</updated><id>https://cthoyt.com/2022/12/19/gcbr-in-bioregistry</id><content type="html" xml:base="https://cthoyt.com/2022/12/19/gcbr-in-bioregistry.html"><![CDATA[<p>The <a href="https://globalbiodata.org">Global Biodata Coalition</a> released a list of
<a href="https://globalbiodata.org/scientific-activities/global-core-biodata-resources"><em>Global Core Biodata Resources (GCBRs)</em></a>
in December 2022, comprising 37 life science databases that they considered as
having significant importance (selected following this
<a href="https://doi.org/10.5281/zenodo.5845116">procedure</a>). While
<a href="https://bioregistry.io">the Bioregistry</a> does not generally cover databases,
many notable databases have one or more associated semantic spaces that are
relevant for inclusion. Accordingly, 33 of 37 of the GCBRs (that’s 89%) have one
or more directly-related prefixes in the Bioregistry. This post gives some
insight into this landscape.</p>

<h2 id="background-on-the-bioregistry">Background on the Bioregistry</h2>

<p>The Bioregistry is a catalog of identifier schema for concepts in the life and
natural sciences. These identifier schemata often arise from databases that
create stable, locally unique identifiers for a given entity type. For example,
the Universal Protein Resource (UniProt) creates stable, locally unique
identifiers for proteins such as <a href="https://bioregistry.io/uniprot:P0DP23">P0DP23</a>
for Calmodulin-1. Similarly, the Chemical Entities of Biological Interest
(ChEBI) creates stable, locally unique identifiers for chemicals such as
<a href="https://bioregistry.io/chebi:138488">138488</a> for alsterpaullone. The
Bioregistry contains records about these identifier schema including the regular
expression pattern that can be used to validate locally unique identifiers
(e.g., the UniProt one is quite complicated but the ChEBI one is simply a string
that looks like a number <code class="language-plaintext highlighter-rouge">^\\d+$</code>), the prefix that should be used when
constructing compact URIs (CURIEs) (e.g., <code class="language-plaintext highlighter-rouge">uniprot</code> for UniProt), a URI format
string that can be used to convert the local unique identifier into a URI (e.g.,
for usage in semantic web applications), and other useful metadata for the
standardization of the identification of life and natural sciences concepts.</p>

<p>Some databases induce more than one identifier schema. For example, in addition
to the identifier schema for proteins, UniProt also has disjoint identifier
schemata for
<a href="https://bioregistry.io/registry/uniprot.location">subcellular locations</a>,
<a href="https://bioregistry.io/registry/uniprot.disease">diseases</a>, and several others.
In the case of UniProt, the main identifier schema is for proteins, and is
therefore given the same prefix as the name of the database (i.e.,
<a href="https://bioregistry.io/uniprot"><code class="language-plaintext highlighter-rouge">uniprot</code></a>). The prefixes for additional
identifier schemata are constructed as <em>subspaces</em> using a dot-delimiter.
Alternatively, some databases that mint multiple identifiers schemata, such as
the
<a href="https://civicdb.org/">Clinical Interpretation of Variants in Cancer (CIViC)</a>
database, do not have a “main” entity type and therefore use subspaces for all
of its prefixes (e.g., <a href="https://bioregistry.io/civic.gid"><code class="language-plaintext highlighter-rouge">civic.gid</code></a> for
genes, <a href="https://bioregistry.io/civic.vid"><code class="language-plaintext highlighter-rouge">civic.vid</code></a> for variants, etc.)</p>

<h2 id="back-to-gcbrs">Back to GCBRs</h2>

<p>It’s important to note that the Bioregistry maintains records for the identifier
schemata, and not the databases themselves. Other catalogs like Wikidata and
<a href="https://fairsharing.org">FAIRsharing</a> already do an excellent job of
maintaining records on databases and other larger efforts. With this background
out of the way, we can return to the main question of this post: how do the
Global Core Biodata Resources (GCBRs) relate to the Bioregistry? I stratified
the list of 37 into four categories:</p>

<ol>
  <li>19 databases that have a single identifier schema (i.e., correspond 1-to-1
with Bioregistry records)</li>
  <li>12 databases that have multiple identifier schemata (i.e., correspond
1-to-many with Bioregistry records)</li>
  <li>2 databases that have a more complicated relationship to Bioregistry records</li>
  <li>4 databases that don’t have identifier schemata (this turns out to be a very
short list!)</li>
</ol>

<h3 id="gcbrs-with-a-single-identifier-schema">GCBRs with a Single Identifier Schema</h3>

<p>The following 19 databases in the GCBR list have a one-to-one correspondence
with a Bioregistry prefix. In some cases, this categorization is partly
subjective as many of these databases’ curators are heavily involved in other
related efforts that their databases heavily reuse. For example, the Zebrafish
Information Network is a model organism database that is heavily involved in the
curation of ontologies for zebrafish anatomy and development (see
<a href="https://biopragmatics/zfa">zfa</a>), developmental stages (see
<a href="https://biopragmatics/zfs">zfs</a>), and phenotypes (see
<a href="https://biopragmatics/zp">zfa</a>).</p>

<table>
  <thead>
    <tr>
      <th>Database</th>
      <th>Bioregisry Prefix</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Alliance of Genome Resources Knowledge Base</td>
      <td><a href="https://bioregistry.io/agrkb"><code class="language-plaintext highlighter-rouge">agrkb</code></a></td>
    </tr>
    <tr>
      <td>Bacterial Diversity Metadatabase</td>
      <td><a href="https://bioregistry.io/bacdive"><code class="language-plaintext highlighter-rouge">bacdive</code></a></td>
    </tr>
    <tr>
      <td>Chemical Entities of Biological Interest</td>
      <td><a href="https://bioregistry.io/chebi"><code class="language-plaintext highlighter-rouge">chebi</code></a></td>
    </tr>
    <tr>
      <td>EcoCyc</td>
      <td><a href="https://bioregistry.io/ecocyc"><code class="language-plaintext highlighter-rouge">ecocyc</code></a></td>
    </tr>
    <tr>
      <td>VEuPathDB ontology</td>
      <td><a href="https://bioregistry.io/eupath"><code class="language-plaintext highlighter-rouge">eupath</code></a></td>
    </tr>
    <tr>
      <td>Global Biodiversity Information Facility</td>
      <td><a href="https://bioregistry.io/gbif"><code class="language-plaintext highlighter-rouge">gbif</code></a></td>
    </tr>
    <tr>
      <td>Genome Aggregation Database</td>
      <td><a href="https://bioregistry.io.gnomad"><code class="language-plaintext highlighter-rouge">gnomad</code></a></td>
    </tr>
    <tr>
      <td>Gene Ontology</td>
      <td><a href="https://bioregistry.io/go"><code class="language-plaintext highlighter-rouge">go</code></a></td>
    </tr>
    <tr>
      <td>InterPro</td>
      <td><a href="https://bioregistry.io/interpro"><code class="language-plaintext highlighter-rouge">interpro</code></a></td>
    </tr>
    <tr>
      <td>Mouse Genome Informatics</td>
      <td><a href="https://bioregistry.io/mgi"><code class="language-plaintext highlighter-rouge">mgi</code></a></td>
    </tr>
    <tr>
      <td>PDB Structure</td>
      <td><a href="https://bioregistry.io/pdb"><code class="language-plaintext highlighter-rouge">pdb</code></a></td>
    </tr>
    <tr>
      <td>European PubMed Central</td>
      <td><a href="https://bioregistry.io/pmc"><code class="language-plaintext highlighter-rouge">pmc</code></a></td>
    </tr>
    <tr>
      <td>PomBase</td>
      <td><a href="https://bioregistry.io/pombase"><code class="language-plaintext highlighter-rouge">pombase</code></a></td>
    </tr>
    <tr>
      <td>ProteomeXchange</td>
      <td><a href="https://bioregistry.io/px"><code class="language-plaintext highlighter-rouge">px</code></a></td>
    </tr>
    <tr>
      <td>Reactome</td>
      <td><a href="https://bioregistry.io/reactome"><code class="language-plaintext highlighter-rouge">reactome</code></a></td>
    </tr>
    <tr>
      <td>Rhea, the Annotated Reactions Database</td>
      <td><a href="https://bioregistry.io/rhea"><code class="language-plaintext highlighter-rouge">rhea</code></a></td>
    </tr>
    <tr>
      <td>Saccharomyces Genome Database</td>
      <td><a href="https://bioregistry.io/sgd"><code class="language-plaintext highlighter-rouge">sgd</code></a></td>
    </tr>
    <tr>
      <td>UCSC Genome Browser</td>
      <td><a href="https://bioregistry.io/ucsc"><code class="language-plaintext highlighter-rouge">ucsc</code></a></td>
    </tr>
    <tr>
      <td>Zebrafish Information Network Gene</td>
      <td><a href="https://bioregistry.io/zfin"><code class="language-plaintext highlighter-rouge">zfin</code></a></td>
    </tr>
  </tbody>
</table>

<h3 id="gcbrs-with-multiple-identifier-schemata">GCBRs with Multiple Identifier Schemata</h3>

<p>The following 12 databases in the GCBR list have identifier schemata and
therefore correspond to multiple Bioregistry records. Note, this list might be
incomplete in cases where there are other relevant identifier schemata that
haven’t been added to the Bioregistry. If you’re aware of one, please let me
know or send a
<a href="https://github.com/biopragmatics/bioregistry/issues/new/choose">new prefix request</a>!</p>

<table>
  <thead>
    <tr>
      <th>Database</th>
      <th>BioregistryPrefix</th>
      <th>Name</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CIViC</td>
      <td><a href="https://bioregistry/civic.aid"><code class="language-plaintext highlighter-rouge">civic.aid</code></a></td>
      <td>CIViC Assertion</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry/civic.did"><code class="language-plaintext highlighter-rouge">civic.did</code></a></td>
      <td>CIViC Disease</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry/civic.eid"><code class="language-plaintext highlighter-rouge">civic.eid</code></a></td>
      <td>CIViC Evidence</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry/civic.gid"><code class="language-plaintext highlighter-rouge">civic.gid</code></a></td>
      <td>CIViC Gene</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry/civic.sid"><code class="language-plaintext highlighter-rouge">civic.sid</code></a></td>
      <td>CIViC Source</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry/civic.tid"><code class="language-plaintext highlighter-rouge">civic.tid</code></a></td>
      <td>CIViC Therapy</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry/civic.vid"><code class="language-plaintext highlighter-rouge">civic.vid</code></a></td>
      <td>CIViC Variant</td>
    </tr>
    <tr>
      <td>BRENDA</td>
      <td><a href="https://bioregistry.io/brenda"><code class="language-plaintext highlighter-rouge">brenda</code></a></td>
      <td>BRENDA Enzmye (duplicate of <code class="language-plaintext highlighter-rouge">eccode</code>)</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/brenda.ligand"><code class="language-plaintext highlighter-rouge">brenda.ligand</code></a></td>
      <td>BRENDA Ligand</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/brenda.ligandgroup"><code class="language-plaintext highlighter-rouge">brenda.ligandgroup</code></a></td>
      <td>BRENDA Ligand Group</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/bto"><code class="language-plaintext highlighter-rouge">bto</code></a></td>
      <td>BRENDA Tissue Ontology</td>
    </tr>
    <tr>
      <td>ChEMBL</td>
      <td><a href="https://bioregistry.io/chembl"><code class="language-plaintext highlighter-rouge">chembl</code></a></td>
      <td>ChEMBL</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/chembl.compound"><code class="language-plaintext highlighter-rouge">chembl.compound</code></a></td>
      <td>ChEMBL Compound (subspace)</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/chembl.target"><code class="language-plaintext highlighter-rouge">chembl.target</code></a></td>
      <td>ChEMBL Target (subspace)</td>
    </tr>
    <tr>
      <td>Ensembl</td>
      <td><a href="https://bioregistry.io/ensembl"><code class="language-plaintext highlighter-rouge">ensembl</code></a></td>
      <td>Ensembl Gene, Transcript, etc.</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/ensembl"><code class="language-plaintext highlighter-rouge">ensemblglossary</code></a></td>
      <td>Ensembl Glossary</td>
    </tr>
    <tr>
      <td>FlyBase</td>
      <td><a href="https://bioregistry.io/flybase"><code class="language-plaintext highlighter-rouge">flybase</code></a></td>
      <td>FlyBase Gene</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/fbbt"><code class="language-plaintext highlighter-rouge">fbbt</code></a></td>
      <td>Drosophila Gross Anatomy</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/fbcv"><code class="language-plaintext highlighter-rouge">fbcv</code></a></td>
      <td>FlyBase Controlled Vocabulary</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/fbrf"><code class="language-plaintext highlighter-rouge">fbrf</code></a></td>
      <td>FlyBase Reference Report</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/fbsp"><code class="language-plaintext highlighter-rouge">fbsp</code></a></td>
      <td>Fly Taxonomy</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/fbtc"><code class="language-plaintext highlighter-rouge">fbtc</code></a></td>
      <td>FlyBase Cell Line</td>
    </tr>
    <tr>
      <td>HGNC</td>
      <td><a href="https://bioregistry.io/hgnc"><code class="language-plaintext highlighter-rouge">hgnc</code></a></td>
      <td>HGNC Gene</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/hgnc.genegroup"><code class="language-plaintext highlighter-rouge">hgnc</code></a></td>
      <td>HGNC Gene Group</td>
    </tr>
    <tr>
      <td>PANTHER</td>
      <td><a href="https://bioregistry.io/panther.family"><code class="language-plaintext highlighter-rouge">panther.family</code></a></td>
      <td>PANTHER Family</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/panther.node"><code class="language-plaintext highlighter-rouge">panther.node</code></a></td>
      <td>PANTHER Node</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/panther.pathway"><code class="language-plaintext highlighter-rouge">panther.pathway</code></a></td>
      <td>PANTHER Pathway</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/panther.pthcmp"><code class="language-plaintext highlighter-rouge">panther.pthcmp</code></a></td>
      <td>PANTHER Pathway Comparison</td>
    </tr>
    <tr>
      <td>PharmGKB</td>
      <td><a href="https://bioregistry.io/pharmgkb.disease"><code class="language-plaintext highlighter-rouge">pharmgkb.disease</code></a></td>
      <td>PharmGKB Disease</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/pharmgkb.drug"><code class="language-plaintext highlighter-rouge">pharmgkb.drug</code></a></td>
      <td>PharmGKB Drug</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/pharmgkb.gene"><code class="language-plaintext highlighter-rouge">pharmgkb.gene</code></a></td>
      <td>PharmGKB Gene</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/pharmgkb.pathways"><code class="language-plaintext highlighter-rouge">pharmgkb.pathways</code></a></td>
      <td>PharmGKB Pathway</td>
    </tr>
    <tr>
      <td>Orphanet</td>
      <td><a href="https://bioregistry.io/orphanet"><code class="language-plaintext highlighter-rouge">orphanet</code></a></td>
      <td>Orphanet</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/orphanet.ordo"><code class="language-plaintext highlighter-rouge">orphanet.ordo</code></a></td>
      <td>Orphanet Rare Disease Ontology</td>
    </tr>
    <tr>
      <td>Rat Genome Database</td>
      <td><a href="https://bioregistry.io/rgd"><code class="language-plaintext highlighter-rouge">rgd</code></a></td>
      <td>Rat Gene</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/rgd.qtl"><code class="language-plaintext highlighter-rouge">rgd.qtl</code></a></td>
      <td>Rat Quantitative Trait Loci</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/rgd.strain"><code class="language-plaintext highlighter-rouge">rgd.strain</code></a></td>
      <td>Rat Strain</td>
    </tr>
    <tr>
      <td>UniProt</td>
      <td><a href="https://bioregistry.io/uniprot"><code class="language-plaintext highlighter-rouge">uniprot</code></a></td>
      <td>UniProt Protein</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/uniprot.arba"><code class="language-plaintext highlighter-rouge">uniprot.arba</code></a></td>
      <td>UniProt Association-Rule-Based Annotator</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/uniprot.chain"><code class="language-plaintext highlighter-rouge">uniprot.chain</code></a></td>
      <td>UniProt Chain</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/uniprot.disease"><code class="language-plaintext highlighter-rouge">uniprot.disease</code></a></td>
      <td>UniProt Disease</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/uniprot.isoform"><code class="language-plaintext highlighter-rouge">uniprot.isoform</code></a></td>
      <td>UniProt Isoform</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/uniprot.keyword"><code class="language-plaintext highlighter-rouge">uniprot.keyword</code></a></td>
      <td>UniProt Keyword</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/uniprot.location"><code class="language-plaintext highlighter-rouge">uniprot.location</code></a></td>
      <td>UniProt Location</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/uniprot.proteome"><code class="language-plaintext highlighter-rouge">uniprot.proteome</code></a></td>
      <td>UniProt Proteome</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/uniprot.ptm"><code class="language-plaintext highlighter-rouge">uniprot.ptm</code></a></td>
      <td>UniProt Post-translational Modification</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/uniprot.resource"><code class="language-plaintext highlighter-rouge">uniprot.resource</code></a></td>
      <td>UniProt Resource</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/uniprot.tissue"><code class="language-plaintext highlighter-rouge">uniprot.tissue</code></a></td>
      <td>UniProt Tissue</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/uniprot.var"><code class="language-plaintext highlighter-rouge">uniprot.var</code></a></td>
      <td>UniProt Variant</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/uniparc"><code class="language-plaintext highlighter-rouge">uniparc</code></a></td>
      <td>UniProt Archive</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/uniref"><code class="language-plaintext highlighter-rouge">uniref</code></a></td>
      <td>UniProt Reference Clusters</td>
    </tr>
    <tr>
      <td>Wormbase</td>
      <td><a href="https://bioregistry.io/wormbase"><code class="language-plaintext highlighter-rouge">wormbase</code></a></td>
      <td>Wormbase Gene</td>
    </tr>
    <tr>
      <td> </td>
      <td><a href="https://bioregistry.io/wormbase.rnai"><code class="language-plaintext highlighter-rouge">wbrnai</code></a></td>
      <td>Wormbase RNAi</td>
    </tr>
  </tbody>
</table>

<h3 id="gcbrs-with-a-confusing-relationship-to-identifier-schemata">GCBRs with a Confusing Relationship to Identifier Schemata</h3>

<p>This list only gets two databases:</p>

<ul>
  <li><a href="https://www.ddbj.nig.ac.jp/">DNA Data Bank of Japan (DDBJ)</a></li>
  <li><a href="https://www.ebi.ac.uk/ena">European Nucleotide Archive (ENA)</a></li>
</ul>

<p>The reason that the DDBJ and ENA (and also
<a href="https://www.ncbi.nlm.nih.gov/genbank/">NCBI GenBank</a>, but it was not included
as a GCBR) have a complicated relationship is because of their involvement in
the
<a href="https://www.insdc.org">International Nucleotide Sequence Database Collaboration (INSDC)</a>.
This is a coordination effort between the DDBJ, EMBL-EBI and NCBI to promote
interoperability between nucleotide sequence and related databases. Here’s a
screenshot from their website that explains how certain services interact across
these platforms:</p>

<p><img src="/img/insdc_map.png" alt="Schematic diagram of INSDC member databases and their relationships" /></p>

<p>Modeling this in the Bioregistry has proven very tricky. There are several
ongoing discussions related to clarifying the relationships between these
databases and various identifier schemata in the Bioregistry’s issue tracker
(see <a href="https://github.com/biopragmatics/bioregistry/issues/108">#108</a>,
<a href="https://github.com/biopragmatics/bioregistry/issues/118">#118</a>, and
<a href="https://github.com/biopragmatics/bioregistry/issues/131">#131</a>). Here are a few
of the relevant prefixes:</p>

<ul>
  <li><a href="https://bioregistry.io/ena.embl"><code class="language-plaintext highlighter-rouge">ena.embl</code></a> (ENA-specific)</li>
  <li><a href="https://bioregistry.io/bioproject"><code class="language-plaintext highlighter-rouge">bioproject</code></a> (DDBJ-specific)</li>
  <li><a href="https://bioregistry.io/registry/genbank"><code class="language-plaintext highlighter-rouge">genbank</code></a> (NCBI-specific)</li>
  <li><a href="https://bioregistry.io/insdc.run"><code class="language-plaintext highlighter-rouge">insdc.run</code></a> (non-specific)</li>
</ul>

<p>If you’ve got some insight into these resources, please join for discussion on
the Bioregistry issue tracker.</p>

<h3 id="gcbrs-with-no-identifier-schemata">GCBRs with No Identifier Schemata</h3>

<p>The four GCBRs that contain no identifier schemata have a common attribute: they
all reuse other identifier schemata.</p>

<ul>
  <li><a href="https://www.clinicalgenome.org/">Clinical Genome Resource</a> (uses
<a href="https://bioregistry.io/hgnc"><code class="language-plaintext highlighter-rouge">hgnc</code></a>,
<a href="https://bioregistry.io/mondo"><code class="language-plaintext highlighter-rouge">mondo</code></a>,
<a href="https://bioregistry.io/pharmgkb.pathways"><code class="language-plaintext highlighter-rouge">pharmgkb.pathways</code></a>, etc.)</li>
  <li><a href="https://www.gencodegenes.org/">GENCODE</a> (uses
<a href="https://bioregistry.io/genbank"><code class="language-plaintext highlighter-rouge">genbank</code></a>)</li>
  <li><a href="https://www.ebi.ac.uk/gwas">GWAS Catalog</a> (uses
<a href="https://bioregistry.io/dbsnp"><code class="language-plaintext highlighter-rouge">dbsnp</code></a>,
<a href="https://bioregistry.io/hgnc.symbol"><code class="language-plaintext highlighter-rouge">hgnc.symbol</code></a>, and
<a href="https://bioregistry.io/efo"><code class="language-plaintext highlighter-rouge">efo</code></a>)</li>
  <li><a href="https://string-db.org">STRING</a> (uses
<a href="https://bioregistry.io/uniprot"><code class="language-plaintext highlighter-rouge">uniprot</code></a>)</li>
</ul>

<p>Interestingly, since these resources transitively use GenBank, dbSNP, and the
Experimental Factor Ontology but these databases were not themselves included in
the list of GCBRs. There are several other examples of databases transitively
used by other GCBRs appearing in other parts of this categorization where this
is also true. Without exhaustively going through all four resources, I thought
I’d use the <a href="https://www.ebi.ac.uk/gwas">GWAS Catalog</a> to illustrate how it
looks when a database re-uses other identifier schemata-providing databases.</p>

<p>First, a genome-wide association study (GWAS) identifies statistical correlation
between genomic markers such as single nucleotide polymorphisms (SNPs) and
disease, phenotypes, or other traits across a large and diverse population,
usually on the scale of thousands to tens of thousands of individuals. The
<a href="https://www.ebi.ac.uk/gwas">GWAS Catalog</a> is an EMBL-EBI database of published
GWASs, their metadata (e.g., experimental design), and the statistically
significant associations they identified. It maintains information about the
following kinds of things:</p>

<table>
  <thead>
    <tr>
      <th>Type</th>
      <th>Example</th>
      <th>Vocabulary</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Variant</td>
      <td><a href="https://www.ebi.ac.uk/gwas/variants/rs7329174">rs7329174</a></td>
      <td>dbSNP (<a href="https://bioregistry.io/dbsnp"><code class="language-plaintext highlighter-rouge">dbsnp</code></a>)</td>
    </tr>
    <tr>
      <td>Gene</td>
      <td><a href="https://www.ebi.ac.uk/gwas/genes/ELF1">ELF1</a></td>
      <td>HGNC Gene Symbols (<a href="https://bioregistry.io/hgnc.symbol"><code class="language-plaintext highlighter-rouge">hgnc.symbol</code></a>)</td>
    </tr>
    <tr>
      <td>Region</td>
      <td><a href="https://www.ebi.ac.uk/gwas/regions/2q37.1">2q37.1</a></td>
      <td>-</td>
    </tr>
    <tr>
      <td>Trait</td>
      <td><a href="https://www.ebi.ac.uk/gwas/efotraits/EFO_0000305">breast carcinoma</a></td>
      <td>Experimental Factor Ontology (<a href="https://bioregistry.io/efo"><code class="language-plaintext highlighter-rouge">efo</code></a>)</td>
    </tr>
  </tbody>
</table>

<p>A given association comprises a <em>p</em>-value for the association between a variant
and a trait. Typically, there is a gene annotated to the SNP to make
interpretation more simple, though this is no easy task. I’d suggest following
Eric Faumann on Twitter (<a href="https://twitter.com/Eric_Fauman">@Eric_Fauman</a>) (or
Mastodon if/when he moves there, because I’m not a big Twitter fan anymore) for
interesting examples of this.</p>

<p>Interestingly, through the process of writing this post, I realized GWAS Catalog
assigns such as <a href="https://www.ebi.ac.uk/gwas/studies/GCST000858">GCST000858</a>
which have their own unique semantic space and provider worthy of an entry in
the Bioregistry, but that will probably have to wait until after winter break.</p>

<h2 id="afterthoughts">Afterthoughts</h2>

<p>I’ve got a few parting comments on the construction of the list of GCBRs:</p>

<ol>
  <li>There appears to be an over-representation of gene/genome and model organism
databases. This probably reflects the fact that these are the oldest kinds of
bioinformatics resources. Overall, the list does include a nice variety of
different kinds of resources.</li>
  <li>There appears to be an over-representation of databases from the EBI, SIB,
and NCBI. Since some of the criteria for inclusion were to demonstrate
existence for more than five years, this makes it difficult for most
databases to even be considered, as most don’t have appropriate funding or
governance models for longevity. It’s also likely that there’s significant
overlap between the professional networks of members of these large,
prominent institutions and the Global Biodata Coalition, which could have
lead to increased interest and familiarity with certain resources that
ultimately were accepted.</li>
  <li>There appears to be an over-representation of American and European
resources. This might be partially due to the criteria that resources were
required to have an English language version, but I think more generally it’s
due to the unfortunate reality that there aren’t many reliable resources
being developed and maintained in other locale for topics in scope for the
GCBR list. I don’t think this is a reflection on a lack of interest for
diversity and inclusion on the part of the Global Biodata Coalition. Further,
efforts like INSDC seem like a good idea to coordinate efforts outside of the
typical western research bubble, and the inclusion of the DDBJ in the list
seems to pay deference to this sentiment.</li>
</ol>

<p>While these observations might be thought-provoking, I’ve only made them at a
subjective level. I think a more interesting follow-up would to be to use linked
data approaches (e.g., through Wikidata) to aggregate relevant information
together about each of these resources and make more objective summaries. To
support that, I’ve created a collection on the Bioregistry
<a href="https://bioregistry.io/collection/0000010">bioregistry.collection:0000010</a> that
contains the list of prefixes mentioned here.</p>

<hr />

<p>Serendipitously, the scientific program committee of the
<a href="https://biocuration2023.github.io">16th Annual International Biocuration Conference (Biocuration 2023)</a>
has invited the Executive Director of the Global Biodata Coalition, Guy
Cochrane, to give a
<a href="https://www.youtube.com/watch?v=Dp952i6T9aI">keynote presentation</a> at the
conference. This will be very timely, and I’m looking forwards to meeting him in
person and chatting about this. If you’re a reader of my blog, you’re likely a
creator, user, or both of resources like the one in the GCBR list. I’d suggest
you consider
<a href="https://biocuration2023.github.io/abstracts">submitting an abstract</a> for the
conference and meeting me in Padua in April!</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="Bioregistry" /><summary type="html"><![CDATA[The Global Biodata Coalition released a list of Global Core Biodata Resources (GCBRs) in December 2022, comprising 37 life science databases that they considered as having significant importance (selected following this procedure). While the Bioregistry does not generally cover databases, many notable databases have one or more associated semantic spaces that are relevant for inclusion. Accordingly, 33 of 37 of the GCBRs (that’s 89%) have one or more directly-related prefixes in the Bioregistry. This post gives some insight into this landscape.]]></summary></entry><entry><title type="html">A First Look at OpenCheck</title><link href="https://cthoyt.com/2022/11/15/opencheck.html" rel="alternate" type="text/html" title="A First Look at OpenCheck" /><published>2022-11-15T18:39:00+00:00</published><updated>2022-11-15T18:39:00+00:00</updated><id>https://cthoyt.com/2022/11/15/opencheck</id><content type="html" xml:base="https://cthoyt.com/2022/11/15/opencheck.html"><![CDATA[<p>There has been legitimate concern about the future of Twitter over the last week
due to its new ownership and management. This is pretty upsetting considering
how great it’s been to use to connect to and to follow other researchers.
<a href="https://web.archive.org/web/20241008073132/https://opencheck.is/">OpenCheck</a> is
currently working to map Twitter handles to
<a href="https://orcid.org">ORCID identifiers</a> and capture the directed follow graph of
researchers on Twitter in case the service becomes unusable in the near future.
This post is about my initial exploration of the resource. <strong>Update in November
2024</strong> - OpenCheck has been shut down.</p>

<h2 id="how-it-works">How It Works</h2>

<p>The OpenCheck website is pretty sleek. There are two big buttons to connect it
to your Twitter account and ORCID account. After authorization, it uses the
Twitter API to look your followers and follows. Then, it looks up if any of
those Twitter accounts have also registered and adds it to
<a href="https://opencheck.is/scitwitter/orcidgraph">this CSV</a> which they make available
under the permissive CC0 license.</p>

<h2 id="wikidata-to-the-rescue">Wikidata to the Rescue</h2>

<p>My first thought was that there are tons of ORCID-Twitter connections available
under the CC0 license through Wikidata that they could use to complement this
content. The following SPARQL query retrieves all of these links:</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="nv">?item</span><span class="w"> </span><span class="nv">?itemLabel</span><span class="w"> </span><span class="nv">?orcid</span><span class="w"> </span><span class="nv">?twitter</span><span class="w">
</span><span class="k">WHERE</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nv">?item</span><span class="w"> </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P31</span><span class="w"> </span><span class="nn">wd</span><span class="o">:</span><span class="ss">Q5</span><span class="p">;</span><span class="w">
        </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P496</span><span class="w"> </span><span class="nv">?orcid</span><span class="p">;</span><span class="w">
        </span><span class="nn">wdt</span><span class="o">:</span><span class="ss">P2002</span><span class="w"> </span><span class="nv">?twitter</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="k">SERVICE</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">label</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nn">bd</span><span class="o">:</span><span class="ss">serviceParam</span><span class="w"> </span><span class="nn">wikibase</span><span class="o">:</span><span class="ss">language</span><span class="w"> </span><span class="s2">"[AUTO_LANGUAGE],en"</span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>This query can be run using the Wikidata Query Service by following
<a href="https://w.wiki/5xaE">this link</a> and pressing the play button. As of November
15th, 2022, it returns 13,784 results.</p>

<h2 id="graph-machine-learning">Graph Machine Learning</h2>

<p>The OpenCheck ORCID graph is a relatively small directed graph, so my second
thought was that would be interesting to apply some graph machine learning
methodologies to it. I wrote a simple script using
<a href="https://github.com/AnacletoLAB/grape">GRAPE</a> to learn low-dimensional vector
representations (i.e., embeddings) for nodes in the graph (i.e., ORCID
identifiers) that could be useful for clustering, classification, or other
downstream machine learning tasks. These resulting embeddings for each ORCID
identifier can be downloaded as a TSV file
<a href="https://github.com/cthoyt/opencheck-embed/raw/main/embeddings/line.tsv">here</a>
and are summarized below:</p>

<p><img src="https://github.com/cthoyt/opencheck-embed/raw/main/embeddings/line.png" alt="" /></p>

<p>I put this script in
<a href="https://github.com/cthoyt/opencheck-embed">cthoyt/opencheck-embed</a> and
additionally configured GitHub Actions to automatically re-run the script every
night to generate new embeddings for the newest dataset.</p>

<p>I had first tested the idea of <em>continuous analysis</em> built on top of GitHub’s
continuous integration service by periodically generating
<a href="https://github.com/reymond-group/drfp">differential reaction fingerprints</a> on
the <a href="https://www.rhea-db.org">Rhea reaction database</a> in
<a href="https://github.com/cthoyt/rhea-fingerprints">cthoyt/rhea-fingerprints</a>. The
OpenCheck ORCID graph is a different scenario because the data are updated in
real time and aren’t versioned. Overall, I think these are both good examples of
what is possible using modern, public infrastructure, and I would like to see
them used in more and more scientific workflows.</p>

<hr />

<p>It appears that OpenCheck is an effort from a company called
<a href="https://www.metarational.net">Metarational, LLC</a> run by
<a href="https://twitter.com/john_d_beatty">John Beatty</a> that has has a more long-term
goal of supporting verification online (i.e., better than an $8 blue star). I’m
excited to see what they do next!</p>

<p>If you want to make sure that your ORCID, Twitter, and other academic profiles
on the internet are linked, check out my <a href="/2021/08/17/self-organization.html">previous
post</a> on curating your own Wikidata
profile.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="bibliometrics" /><category term="social media" /><summary type="html"><![CDATA[There has been legitimate concern about the future of Twitter over the last week due to its new ownership and management. This is pretty upsetting considering how great it’s been to use to connect to and to follow other researchers. OpenCheck is currently working to map Twitter handles to ORCID identifiers and capture the directed follow graph of researchers on Twitter in case the service becomes unusable in the near future. This post is about my initial exploration of the resource. Update in November 2024 - OpenCheck has been shut down.]]></summary></entry><entry><title type="html">Curating Publications on Wikidata</title><link href="https://cthoyt.com/2022/02/12/wikidata-publications.html" rel="alternate" type="text/html" title="Curating Publications on Wikidata" /><published>2022-02-12T18:10:00+00:00</published><updated>2022-02-12T18:10:00+00:00</updated><id>https://cthoyt.com/2022/02/12/wikidata-publications</id><content type="html" xml:base="https://cthoyt.com/2022/02/12/wikidata-publications.html"><![CDATA[<p>This blog post is a tutorial on how to curate the links between a researcher and
scholarly works (e.g., pre-prints, publications, presentations) on Wikidata
using <a href="https://scholia.toolforge.org/">Scholia</a> and the
<a href="https://author-disambiguator.toolforge.org">Author Disambiguator</a> tool.</p>

<h2 id="ensure-content-is-available">Ensure content is available</h2>

<p>While Wikidata is full of useful scholarly information, it’s up to the community
to include new content. Therefore, before we go about curating links between
your Wikidata entry and your publications, we first have to ensure there are
Wikidata entries for your publications. There are several ways to do this, but
this tutorial will focus on getting the list of your publications from your
citation manager.</p>

<p>This tutorial will assume you’re using <a href="https://www.mendeley.com">Mendeley</a>, a
citation manager that can export EndNote XML documents. This tutorial might be
later extended to include an example with <a href="https://www.zotero.org/">Zotero</a>,
which can also export EndNote XML documents. After opening the application,
follow these instructions:</p>

<p><img src="/img/wikidata-publications/mendeley.png" alt="" /></p>

<ol>
  <li>Select the publications you want to put into Wikidata</li>
  <li>Right click on one of them</li>
  <li>Select “export”</li>
  <li>Choose “EndNote XML” as the filetype</li>
</ol>

<h2 id="uploading-to-wikidata">Uploading to Wikidata</h2>

<ol>
  <li>Create an account on <a href="https://www.wikidata.org">Wikidata</a>. Keep in mind this
is <em>not</em> the same as the Wikidata entry about you.</li>
  <li>
    <p>Store your Wikidata credentials in a configuration file at
<code class="language-plaintext highlighter-rouge">~/.config/wikidata.ini</code> (where <code class="language-plaintext highlighter-rouge">~</code> means your home directory). It should
look like:</p>

    <div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[wikidata]</span>
<span class="py">username</span> <span class="p">=</span> <span class="s">&lt;your username here&gt;</span>
<span class="py">password</span> <span class="p">=</span> <span class="s">&lt;your password here&gt;</span>
</code></pre></div>    </div>
  </li>
  <li>Install <a href="https://www.python.org">Python</a>.</li>
  <li>
    <p>Run the following in your terminal:</p>

    <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pip <span class="nb">install </span>citation-url[endnote]
<span class="nv">$ </span>python <span class="nt">-m</span> citation_url.endnote &lt;LOCAL FILE PATH TO YOUR ENDNOTE FILE&gt;
</code></pre></div>    </div>
  </li>
</ol>

<p>This will now parse your EndNote XML file and do its best to populate Wikidata
with all the DOIs, PubMed identifiers, PMC identifiers, arXiv identifiers, PDF
links, and other references.</p>

<h2 id="update-your-wikidata-entry">Update your Wikidata entry</h2>

<p>This tutorial already assumes you’ve created a Wikidata entry. If you haven’t
done this already, go back and read <a href="/2021/08/17/self-organization.html">this
tutorial</a>. Before continuing, make
sure that you’ve edited the “synonyms” on your Wikidata entry so it has a wide
variety of how your name might have been written in publication. This means
different ordering of first/last name, different usages of single letters versus
writing out in full, sometimes omitting the middle name, using dots after
letters or not, etc.</p>

<p><img src="/img/wikidata_researcher_synonyms.png" alt="" /></p>

<p>This step is crucial because the curation interface in the next step relies on
direct string matching between this list of synonyms and what the Wikidata
Integrator was able to pull from CrossRef, EuropePMC, and other metadata
resources.</p>

<h2 id="curate-wikidata-using-the-author-disambiguator">Curate Wikidata using the Author Disambiguator</h2>

<ol>
  <li>Follow
<a href="https://author-disambiguator.toolforge.org/names_oauth.php?action=authorize">this link</a>
to sign in to the Author Disambiguator tool using your Wikidata
username/password. We’ll need this for later steps.</li>
  <li><a href="https://scholia.toolforge.org">Scholia</a> is a frontend for navigating
scholarly data within Wikidata. Navigating to your Scholia curation by
replacing <code class="language-plaintext highlighter-rouge">&lt;YOUR Q NUMBER&gt;</code> in
<code class="language-plaintext highlighter-rouge">https://scholia.toolforge.org/author/&lt;YOUR Q NUMBER&gt;/curation#missing-author-resolving</code>
with your Wikidata identifier. It starts with a Q followed by some numbers
coming directly after the title on your page. For example, mine is
<code class="language-plaintext highlighter-rouge">Q47475003</code>. You can follow
<a href="https://scholia.toolforge.org/author/Q47475003/curation#missing-author-resolving">this link</a>
to see my curation page on Scholia as an example to make sure you’re in the
right place for yours.</li>
  <li>
    <p>Click the link next to the specific text string you want to curate:</p>

    <p><img src="/img/wikidata-publications/scholia-interface.png" alt="" /></p>
  </li>
  <li>
    <p>This will bring you to the
<a href="https://author-disambiguator.toolforge.org">Author Disambiguator</a> page.
Click the checkboxes on the publications that are yours (1), or use the boxes
to check entire groups (2).</p>

    <p><img src="/img/wikidata-publications/select.png" alt="" /></p>
  </li>
  <li>
    <p>Scroll down past all the check boxes to the section labeled <em>Potential Author
Items</em>. Select the radio box corresponding to your Wikidata entry (1), then
click <em>Link Selected Works to Author</em> (2).</p>

    <p><img src="/img/wikidata-publications/finish.png" alt="" /></p>
  </li>
  <li>A new window will appear so you can track the status of the job, but you can
close it and it will finish in the background. Give a few minutes, then your
profile on Scholia will be updated.</li>
</ol>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="bibliometrics" /><summary type="html"><![CDATA[This blog post is a tutorial on how to curate the links between a researcher and scholarly works (e.g., pre-prints, publications, presentations) on Wikidata using Scholia and the Author Disambiguator tool.]]></summary></entry><entry><title type="html">You Should Use a Private Email on Publications</title><link href="https://cthoyt.com/2022/02/06/use-your-personal-email.html" rel="alternate" type="text/html" title="You Should Use a Private Email on Publications" /><published>2022-02-06T21:55:00+00:00</published><updated>2022-02-06T21:55:00+00:00</updated><id>https://cthoyt.com/2022/02/06/use-your-personal-email</id><content type="html" xml:base="https://cthoyt.com/2022/02/06/use-your-personal-email.html"><![CDATA[<p>While we were recently preparing to submit a manuscript, the lead author said
they looked at my last few papers and noticed I always used a private email
address instead of an institutional email address. They asked, perplexed, if
they should also use my private email address with our submission. The answer
was a resounding <em>yes</em>; always use a private email address. Here’s why.</p>

<p>I actually started thinking about this way back one thousand years ago in 2020
and started an interesting discussion on Twitter:</p>

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="en" dir="ltr">Lesson for young researchers - don’t use your institutional email address on papers.<br /><br />You *will* leave, you won’t get to keep it, and you’ll miss out on lots of people who want to talk to you because of the interesting work you did.<br /><br />Try <a href="https://twitter.com/ORCID_Org?ref_src=twsrc%5Etfw">@ORCID_Org</a> instead :) <a href="https://twitter.com/hashtag/AcademicChatter?src=hash&amp;ref_src=twsrc%5Etfw">#AcademicChatter</a></p>&mdash; Charles Tapley Hoyt (@cthoyt) <a href="https://twitter.com/cthoyt/status/1275566074902335490?ref_src=twsrc%5Etfw">June 23, 2020</a></blockquote>

<p>I came back to this again when submitting the
<a href="https://github.com/indralab/gilda">Gilda</a> manuscript at the end of 2021, then
got a distracted by an ankle injury and sort of lost track of all the blog posts
I had been writing. Now I’m finishing it up in early February 2022. The rest of
this post is an elaboration on my ideas and follow-up discussion on Twitter.</p>

<h2 id="you-cant-take-it-with-you">You can’t take it with you</h2>

<p>If you’re a researcher who sometimes writes and submits publications, the
chances are pretty high that you currently work at some kind of institution and
will not always work at that institution. Here’s what might happen to your
institutional email address when you leave:</p>

<ol>
  <li>Your old institution doesn’t care about you after you leave, and deletes your
account the moment you walk out the door.</li>
  <li>Your old institution doesn’t care about you after you leave, and says that
they will continue your access for a limited amount of time after you leave
to save face. Then it deletes your account.</li>
  <li>Your old institution doesn’t care about you after you leave, and says that
they will continue to provide technical support to you and all previous
employees indefinitely with their infinite money and benevolence. I’m being
sarcastic; this is really, really unlikely.</li>
</ol>

<p>Whether 1, 2, or 3, if you used your institutional email address on a paper you
published, it is now a dead link into the abyss. Anyone who might want to get in
touch with you to chat about your research (or god forbid, ask you for code or
data that you didn’t deposit in an appropriate place before publishing).</p>

<p>If you had used your personal email address, which won’t go away, then you
wouldn’t have this problem.</p>

<p>Alternatively, some publishers allow you to annotate your ORCID identifier on to
the manuscript, then you could potentially maintain your current working email
address through ORCID, but again, not a lot of publishers support this (yet).</p>

<h2 id="who-does-this-most-affect">Who does this most affect?</h2>

<p>As you become more senior, the chances of you moving institutions decreases. So
this is an issue that disproportionately affects young researchers twice: first
because you are harder to reach and second because your ability to network is
more crucial as a young researcher than to an veteran one.</p>

<h2 id="do-your-best-to-disregard-institutional-policy">Do your best to disregard institutional policy</h2>

<p>Institutions obviously want as much attribution as possible when you publish
while working there, and using a prominent email address is one way to get that
goal. Therefore, many institutions have a policy that you have to use your
institutional email when publishing. A few courses of actions you could take:</p>

<ol>
  <li>Ask the editor to include multiple email addresses</li>
  <li>Disregard institutional policy, it doesn’t support you as a researcher or a
person.</li>
</ol>

<h2 id="im-worried-about-getting-a-bunch-of-spam">I’m worried about getting a bunch of spam</h2>

<p>I can’t speak for all situations, but I’ve been using my personal gmail address
all over the internet and still haven’t got a ton of spam. It’s sitting at the
bottom of this blog post if you want to prove me wrong. If you’re worried about
this, make a new private email account you just use for publishing.</p>

<h2 id="difficulties-for-editors">Difficulties for editors</h2>

<p>One
<a href="https://twitter.com/JohnRYatesIII/status/1275791480234237954?s=20&amp;t=9tkF3mhrQ12uXaP3e7G_uA">follow-up conversation</a>
I had on Twitter was with John R. Yates III, the editor-in-chief of the
<a href="https://pubs.acs.org/journal/jprobs">Journal of Proteome Research</a>. He gave
interesting insight that editors highly prefer institutional email addresses
because they are perceived to be more trustworthy. There are several reasons why
this is not true (e.g., you could use an outdated address, spoof it, etc.) but
ultimately this serves to distract from adopting more sustainable ways of
identifying authors and reviewers like with ORCID.</p>

<h2 id="relevant-twitter-threads">Relevant Twitter Threads</h2>

<ul>
  <li>https://twitter.com/travisdrake/status/1577970169951010817</li>
</ul>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="bibliometrics" /><summary type="html"><![CDATA[While we were recently preparing to submit a manuscript, the lead author said they looked at my last few papers and noticed I always used a private email address instead of an institutional email address. They asked, perplexed, if they should also use my private email address with our submission. The answer was a resounding yes; always use a private email address. Here’s why.]]></summary></entry><entry><title type="html">Abstracting the parameters of a Machine Learning Model</title><link href="https://cthoyt.com/2022/02/06/model-abstraction.html" rel="alternate" type="text/html" title="Abstracting the parameters of a Machine Learning Model" /><published>2022-02-06T18:00:00+00:00</published><updated>2022-02-06T18:00:00+00:00</updated><id>https://cthoyt.com/2022/02/06/model-abstraction</id><content type="html" xml:base="https://cthoyt.com/2022/02/06/model-abstraction.html"><![CDATA[<p>As a follow-up to my <a href="/2022/02/06/model-refactoring.html">previous
post</a> on refactoring and improving
a machine learning model implemented with <a href="https://pytorch.org">PyTorch</a>, this
post will be a tutorial on how to generalize the implementation of a
<a href="https://en.wikipedia.org/wiki/Multilayer_perceptron">multilayer perceptron (MLP)</a>
to use one of several potential non-linear activation functions in an elegant
way.</p>

<p>We’ll pick up with the seventh (final) model version of the MLP from my previous
post:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">itertools</span> <span class="kn">import</span> <span class="n">chain</span>

<span class="kn">from</span> <span class="nn">more_itertools</span> <span class="kn">import</span> <span class="n">pairwise</span>
<span class="kn">from</span> <span class="nn">torch</span> <span class="kn">import</span> <span class="n">nn</span>


<span class="k">class</span> <span class="nc">MLP7</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dims</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="o">*</span><span class="n">chain</span><span class="p">.</span><span class="n">from_iterable</span><span class="p">(</span>
            <span class="p">(</span>
                <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">),</span>
                <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">(),</span>
            <span class="p">)</span>
            <span class="k">for</span> <span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span> <span class="ow">in</span> <span class="n">pairwise</span><span class="p">(</span><span class="n">dims</span><span class="p">)</span>
        <span class="p">))</span>
</code></pre></div></div>

<h2 id="incremental-improvements">Incremental Improvements</h2>

<p>This MLP uses a hard-coded
<a href="https://en.wikipedia.org/wiki/Rectifier_(neural_networks)">rectified linear unit</a>
as the non-linear activation function between layers. We can initially
generalize MLP7 to use a variety of non-linear activation functions by adding an
argument to its <code class="language-plaintext highlighter-rouge">__init__()</code> function:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">itertools</span> <span class="kn">import</span> <span class="n">chain</span>

<span class="kn">from</span> <span class="nn">more_itertools</span> <span class="kn">import</span> <span class="n">pairwise</span>
<span class="kn">from</span> <span class="nn">torch</span> <span class="kn">import</span> <span class="n">nn</span>


<span class="k">class</span> <span class="nc">MLP8</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dims</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">],</span> <span class="n">activation</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"relu"</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">activation</span> <span class="o">==</span> <span class="s">"relu"</span><span class="p">:</span>
            <span class="n">activation</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">()</span>
        <span class="k">elif</span> <span class="n">activation</span> <span class="o">==</span> <span class="s">"tanh"</span><span class="p">:</span>
            <span class="n">activation</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Tanh</span><span class="p">()</span>
        <span class="k">elif</span> <span class="n">activation</span> <span class="o">==</span> <span class="s">"hardtanh"</span><span class="p">:</span>
            <span class="n">activation</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Hardtanh</span><span class="p">()</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nb">KeyError</span><span class="p">(</span><span class="sa">f</span><span class="s">"Unsupported activation: </span><span class="si">{</span><span class="n">activation</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="n">chain</span><span class="p">.</span><span class="n">from_iterable</span><span class="p">(</span>
            <span class="p">(</span>
                <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">),</span>
                <span class="n">activation</span><span class="p">,</span>
            <span class="p">)</span>
            <span class="k">for</span> <span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span> <span class="ow">in</span> <span class="n">pairwise</span><span class="p">(</span><span class="n">dims</span><span class="p">)</span>
        <span class="p">))</span>
</code></pre></div></div>

<p>The first issue with this MLP8 is it relies on a hard-coded set of conditional
statements and is therefore hard to extend. It can be improved by using a
dictionary lookup:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">itertools</span> <span class="kn">import</span> <span class="n">chain</span>

<span class="kn">from</span> <span class="nn">more_itertools</span> <span class="kn">import</span> <span class="n">pairwise</span>
<span class="kn">from</span> <span class="nn">torch</span> <span class="kn">import</span> <span class="n">nn</span>

<span class="n">activation_lookup</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">"relu"</span><span class="p">:</span> <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">(),</span>
    <span class="s">"tanh"</span><span class="p">:</span> <span class="n">nn</span><span class="p">.</span><span class="n">Tanh</span><span class="p">(),</span>
    <span class="s">"hardtanh"</span><span class="p">:</span> <span class="n">nn</span><span class="p">.</span><span class="n">Hardtanh</span><span class="p">(),</span>
<span class="p">}</span>


<span class="k">class</span> <span class="nc">MLP9</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dims</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">],</span> <span class="n">activation</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"relu"</span><span class="p">):</span>
        <span class="n">activation</span> <span class="o">=</span> <span class="n">activation_lookup</span><span class="p">[</span><span class="n">activation</span><span class="p">]</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="n">chain</span><span class="p">.</span><span class="n">from_iterable</span><span class="p">(</span>
            <span class="p">(</span>
                <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">),</span>
                <span class="n">activation</span><span class="p">,</span>
            <span class="p">)</span>
            <span class="k">for</span> <span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span> <span class="ow">in</span> <span class="n">pairwise</span><span class="p">(</span><span class="n">dims</span><span class="p">)</span>
        <span class="p">))</span>
</code></pre></div></div>

<p>Unfortunately, the approach in MLP9 rigid because it requires pre-instantiation
of the activations. If we needed to vary the arguments to the <code class="language-plaintext highlighter-rouge">nn.HardTanh</code>
class (i.e., the minimum and maximum values), the previous approach wouldn’t
work. We can change the implementation to lookup on the class <em>before
instantiation</em> then optionally pass some arguments:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">itertools</span> <span class="kn">import</span> <span class="n">chain</span>

<span class="kn">from</span> <span class="nn">more_itertools</span> <span class="kn">import</span> <span class="n">pairwise</span>
<span class="kn">from</span> <span class="nn">torch</span> <span class="kn">import</span> <span class="n">nn</span>

<span class="n">activation_lookup</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">type</span><span class="p">[</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">]]</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">"relu"</span><span class="p">:</span> <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">,</span>
    <span class="s">"tanh"</span><span class="p">:</span> <span class="n">nn</span><span class="p">.</span><span class="n">Tanh</span><span class="p">,</span>
    <span class="s">"hardtanh"</span><span class="p">:</span> <span class="n">nn</span><span class="p">.</span><span class="n">Hardtanh</span><span class="p">,</span>
<span class="p">}</span>


<span class="k">class</span> <span class="nc">MLP10</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span>
        <span class="bp">self</span><span class="p">,</span>
        <span class="n">dims</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">],</span>
        <span class="n">activation</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"relu"</span><span class="p">,</span>
        <span class="n">activation_kwargs</span><span class="p">:</span> <span class="bp">None</span> <span class="o">|</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">any</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
    <span class="p">):</span>
        <span class="n">activation_cls</span> <span class="o">=</span> <span class="n">activation_lookup</span><span class="p">[</span><span class="n">activation</span><span class="p">]</span>
        <span class="n">activation</span> <span class="o">=</span> <span class="n">activation_cls</span><span class="p">(</span><span class="o">**</span><span class="p">(</span><span class="n">activation_kwargs</span> <span class="ow">or</span> <span class="p">{}))</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="n">chain</span><span class="p">.</span><span class="n">from_iterable</span><span class="p">(</span>
            <span class="p">(</span>
                <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">),</span>
                <span class="n">activation</span><span class="p">,</span>
            <span class="p">)</span>
            <span class="k">for</span> <span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span> <span class="ow">in</span> <span class="n">pairwise</span><span class="p">(</span><span class="n">dims</span><span class="p">)</span>
        <span class="p">))</span>
</code></pre></div></div>

<p>MLP10 is a big improvement in terms of flexibility, but it has a few remaining
practical issues:</p>

<ol>
  <li>you have to manually maintain the <code class="language-plaintext highlighter-rouge">activation_lookup</code> dictionary,</li>
  <li>you can’t pass a pre-instantiated instance of an activation class via the
<code class="language-plaintext highlighter-rouge">activation</code> argument</li>
  <li>you have to get the casing of the string keys just right</li>
  <li>the default is hard-coded as a string, which means this has to get copied
(error-prone) in any place that creates an MLP</li>
  <li>you have to re-write this logic for all of your classes</li>
</ol>

<h2 id="the-class-resolver">The <code class="language-plaintext highlighter-rouge">class-resolver</code></h2>

<p>Before showing MLP11, the final solution, I want to first describe the
<a href="https://github.com/cthoyt/class-resolver"><code class="language-plaintext highlighter-rouge">class-resolver</code></a> package. Its job is
to make it easy to generate a dictionary-like object that you can use to look up
classes (like we prepared for MLP10). It’s smart and takes care of several
things for you:</p>

<ol>
  <li>Automatically assigns keys in the dictionary based on the class name. If all
the classes in the <em>resolver</em> share a suffix, it automatically strips it.</li>
  <li>It uses some simple string normalization during lookup, so it’s insensitive
to capitalization, varied usage of underscores, or other punctuation.</li>
  <li>It keeps track of a default value to grab when you pass <code class="language-plaintext highlighter-rouge">None</code></li>
  <li>It allows for classes and instances to be passed through</li>
</ol>

<p>After making a <code class="language-plaintext highlighter-rouge">ClassResolver</code> instance, you can use the
<code class="language-plaintext highlighter-rouge">ClassResolver.lookup()</code> function to get the class you need:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">class_resolver</span> <span class="kn">import</span> <span class="n">ClassResolver</span>
<span class="kn">from</span> <span class="nn">torch</span> <span class="kn">import</span> <span class="n">nn</span>

<span class="n">activation_resolver</span> <span class="o">=</span> <span class="n">ClassResolver</span><span class="p">(</span>
    <span class="p">[</span><span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">,</span> <span class="n">nn</span><span class="p">.</span><span class="n">Tanh</span><span class="p">,</span> <span class="n">nn</span><span class="p">.</span><span class="n">Hardtanh</span><span class="p">],</span>
    <span class="n">base</span><span class="o">=</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">,</span>
    <span class="n">default</span><span class="o">=</span><span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">,</span>
<span class="p">)</span>

<span class="c1"># Default lookup
</span><span class="k">assert</span> <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span> <span class="o">==</span> <span class="n">activation_resolver</span><span class="p">.</span><span class="n">lookup</span><span class="p">(</span><span class="bp">None</span><span class="p">)</span>

<span class="c1"># Name-based lookup
</span><span class="k">assert</span> <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span> <span class="o">==</span> <span class="n">activation_resolver</span><span class="p">.</span><span class="n">lookup</span><span class="p">(</span><span class="s">"relu"</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span> <span class="o">==</span> <span class="n">activation_resolver</span><span class="p">.</span><span class="n">lookup</span><span class="p">(</span><span class="s">"ReLU"</span><span class="p">)</span>

<span class="c1"># Class-based lookup
</span><span class="k">assert</span> <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span> <span class="o">==</span> <span class="n">activation_resolver</span><span class="p">.</span><span class="n">lookup</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">)</span>
</code></pre></div></div>

<p>Built on top of the <code class="language-plaintext highlighter-rouge">ClassResolver.lookup()</code> function is the
<code class="language-plaintext highlighter-rouge">ClassResolver.make()</code> function, which first looks up the class, then gives you
an instance of it (optionally using keyword arguments you pass).</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Default instantiation
</span><span class="k">assert</span> <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">()</span> <span class="o">==</span> <span class="n">activation_resolver</span><span class="p">.</span><span class="n">make</span><span class="p">(</span><span class="bp">None</span><span class="p">)</span>

<span class="c1"># Name-based instantiation
</span><span class="k">assert</span> <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">()</span> <span class="o">==</span> <span class="n">activation_resolver</span><span class="p">.</span><span class="n">make</span><span class="p">(</span><span class="s">"relu"</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">()</span> <span class="o">==</span> <span class="n">activation_resolver</span><span class="p">.</span><span class="n">make</span><span class="p">(</span><span class="s">"ReLU"</span><span class="p">)</span>

<span class="c1"># Class-based instantiation
</span><span class="k">assert</span> <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">()</span> <span class="o">==</span> <span class="n">activation_resolver</span><span class="p">.</span><span class="n">make</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">)</span>

<span class="c1"># Class-based instantiation w/ keyword arguments
</span><span class="k">assert</span> <span class="n">nn</span><span class="p">.</span><span class="n">Hardtanh</span><span class="p">(</span><span class="mf">0.0</span><span class="p">,</span> <span class="mf">6.0</span><span class="p">)</span> <span class="o">==</span> <span class="n">activation_resolver</span><span class="p">.</span><span class="n">make</span><span class="p">(</span><span class="s">"hardtanh"</span><span class="p">,</span> <span class="p">{</span>
    <span class="s">"min_val"</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span> <span class="s">"max_value"</span><span class="p">:</span> <span class="mf">6.0</span>
<span class="p">})</span>
</code></pre></div></div>

<h2 id="bringing-it-all-together">Bringing it All Together</h2>

<p>Let’s apply that to MLP10 and make our final MLP11:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">itertools</span> <span class="kn">import</span> <span class="n">chain</span>

<span class="kn">from</span> <span class="nn">class_resolver</span> <span class="kn">import</span> <span class="n">ClassResolver</span>
<span class="kn">from</span> <span class="nn">more_itertools</span> <span class="kn">import</span> <span class="n">pairwise</span>
<span class="kn">from</span> <span class="nn">torch</span> <span class="kn">import</span> <span class="n">nn</span>

<span class="n">activation_resolver</span> <span class="o">=</span> <span class="n">ClassResolver</span><span class="p">(</span>
    <span class="p">[</span><span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">,</span> <span class="n">nn</span><span class="p">.</span><span class="n">Tanh</span><span class="p">,</span> <span class="n">nn</span><span class="p">.</span><span class="n">Hardtanh</span><span class="p">],</span>
    <span class="n">base</span><span class="o">=</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">,</span>
    <span class="n">default</span><span class="o">=</span><span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">,</span>
<span class="p">)</span>


<span class="k">class</span> <span class="nc">MLP11</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span>
        <span class="bp">self</span><span class="p">,</span>
        <span class="n">dims</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">],</span>
        <span class="n">activation</span><span class="p">:</span> <span class="bp">None</span> <span class="o">|</span> <span class="nb">str</span> <span class="o">|</span> <span class="n">nn</span><span class="p">.</span><span class="n">Module</span> <span class="o">|</span> <span class="nb">type</span><span class="p">[</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
        <span class="n">activation_kwargs</span><span class="p">:</span> <span class="bp">None</span> <span class="o">|</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">any</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
    <span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="n">chain</span><span class="p">.</span><span class="n">from_iterable</span><span class="p">(</span>
            <span class="p">(</span>
                <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">),</span>
                <span class="n">activation_resolver</span><span class="p">.</span><span class="n">make</span><span class="p">(</span><span class="n">activation</span><span class="p">,</span> <span class="n">activation_kwargs</span><span class="p">),</span>
            <span class="p">)</span>
            <span class="k">for</span> <span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span> <span class="ow">in</span> <span class="n">pairwise</span><span class="p">(</span><span class="n">dims</span><span class="p">)</span>
        <span class="p">))</span>
</code></pre></div></div>

<p>Now, you can instantiate the MLP with any of the following:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">MLP11</span><span class="p">(</span><span class="n">dims</span><span class="o">=</span><span class="p">[</span><span class="mi">10</span><span class="p">,</span> <span class="mi">200</span><span class="p">,</span> <span class="mi">40</span><span class="p">])</span>  <span class="c1"># uses default, which is ReLU
</span><span class="n">MLP11</span><span class="p">(</span><span class="n">dims</span><span class="o">=</span><span class="p">[</span><span class="mi">10</span><span class="p">,</span> <span class="mi">200</span><span class="p">,</span> <span class="mi">40</span><span class="p">],</span> <span class="n">activation</span><span class="o">=</span><span class="s">"relu"</span><span class="p">)</span>  <span class="c1"># uses lowercase
</span><span class="n">MLP11</span><span class="p">(</span><span class="n">dims</span><span class="o">=</span><span class="p">[</span><span class="mi">10</span><span class="p">,</span> <span class="mi">200</span><span class="p">,</span> <span class="mi">40</span><span class="p">],</span> <span class="n">activation</span><span class="o">=</span><span class="s">"ReLU"</span><span class="p">)</span>  <span class="c1"># uses stylized
</span><span class="n">MLP11</span><span class="p">(</span><span class="n">dims</span><span class="o">=</span><span class="p">[</span><span class="mi">10</span><span class="p">,</span> <span class="mi">200</span><span class="p">,</span> <span class="mi">40</span><span class="p">],</span> <span class="n">activation</span><span class="o">=</span><span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">)</span>  <span class="c1"># uses class
</span><span class="n">MLP11</span><span class="p">(</span><span class="n">dims</span><span class="o">=</span><span class="p">[</span><span class="mi">10</span><span class="p">,</span> <span class="mi">200</span><span class="p">,</span> <span class="mi">40</span><span class="p">],</span> <span class="n">activation</span><span class="o">=</span><span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">())</span>  <span class="c1"># uses instance
</span>
<span class="n">MLP11</span><span class="p">(</span><span class="n">dims</span><span class="o">=</span><span class="p">[</span><span class="mi">10</span><span class="p">,</span> <span class="mi">200</span><span class="p">,</span> <span class="mi">40</span><span class="p">],</span> <span class="n">activation</span><span class="o">=</span><span class="s">"hardtanh"</span><span class="p">,</span>
      <span class="n">activation_kwargs</span><span class="o">=</span><span class="p">{</span><span class="s">"min_val"</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span> <span class="s">"max_value"</span><span class="p">:</span> <span class="mf">6.0</span><span class="p">})</span>  <span class="c1"># uses kwargs
</span><span class="n">MLP11</span><span class="p">(</span><span class="n">dims</span><span class="o">=</span><span class="p">[</span><span class="mi">10</span><span class="p">,</span> <span class="mi">200</span><span class="p">,</span> <span class="mi">40</span><span class="p">],</span> <span class="n">activation</span><span class="o">=</span><span class="n">nn</span><span class="p">.</span><span class="n">HardTanh</span><span class="p">,</span>
      <span class="n">activation_kwargs</span><span class="o">=</span><span class="p">{</span><span class="s">"min_val"</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span> <span class="s">"max_value"</span><span class="p">:</span> <span class="mf">6.0</span><span class="p">})</span>  <span class="c1"># uses kwargs
</span><span class="n">MLP11</span><span class="p">(</span><span class="n">dims</span><span class="o">=</span><span class="p">[</span><span class="mi">10</span><span class="p">,</span> <span class="mi">200</span><span class="p">,</span> <span class="mi">40</span><span class="p">],</span> <span class="n">activation</span><span class="o">=</span><span class="n">nn</span><span class="p">.</span><span class="n">HardTanh</span><span class="p">(</span><span class="mf">0.0</span><span class="p">,</span> <span class="mf">6.0</span><span class="p">))</span>  <span class="c1"># uses instance
</span></code></pre></div></div>

<p>In practice, it makes sense to stick to using the strings in combination with
hyper-parameter optimization libraries like <a href="https://optuna.org/">Optuna</a>.</p>

<hr />

<p>Because the usage of <code class="language-plaintext highlighter-rouge">class-resolver</code> for resolving activation functions from
PyTorch is so common, we’ve made it available through contrib module in
<code class="language-plaintext highlighter-rouge">class_resolver.contrib.torch</code>. In fact, the <code class="language-plaintext highlighter-rouge">activation_resolver</code> comes with
some extra logic to automatically grab <em>all</em> activation modules from
<code class="language-plaintext highlighter-rouge">torch.nn.modules.activation</code>. Therefore, we can rewrite the example for MLP11
to simply import it.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">itertools</span> <span class="kn">import</span> <span class="n">chain</span>

<span class="kn">from</span> <span class="nn">class_resolver.contrib.torch</span> <span class="kn">import</span> <span class="n">activation_resolver</span>
<span class="kn">from</span> <span class="nn">more_itertools</span> <span class="kn">import</span> <span class="n">pairwise</span>
<span class="kn">from</span> <span class="nn">torch</span> <span class="kn">import</span> <span class="n">nn</span>


<span class="k">class</span> <span class="nc">MLP</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span>
        <span class="bp">self</span><span class="p">,</span>
        <span class="n">dims</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">],</span>
        <span class="n">activation</span><span class="p">:</span> <span class="bp">None</span> <span class="o">|</span> <span class="nb">str</span> <span class="o">|</span> <span class="n">nn</span><span class="p">.</span><span class="n">Module</span> <span class="o">|</span> <span class="nb">type</span><span class="p">[</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
        <span class="n">activation_kwargs</span><span class="p">:</span> <span class="bp">None</span> <span class="o">|</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">any</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
    <span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="n">chain</span><span class="p">.</span><span class="n">from_iterable</span><span class="p">(</span>
            <span class="p">(</span>
                <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">),</span>
                <span class="n">activation_resolver</span><span class="p">.</span><span class="n">make</span><span class="p">(</span><span class="n">activation</span><span class="p">,</span> <span class="n">activation_kwargs</span><span class="p">),</span>
            <span class="p">)</span>
            <span class="k">for</span> <span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span> <span class="ow">in</span> <span class="n">pairwise</span><span class="p">(</span><span class="n">dims</span><span class="p">)</span>
        <span class="p">))</span>
</code></pre></div></div>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="programming" /><category term="machine learning" /><category term="deep learning" /><category term="Python" /><summary type="html"><![CDATA[As a follow-up to my previous post on refactoring and improving a machine learning model implemented with PyTorch, this post will be a tutorial on how to generalize the implementation of a multilayer perceptron (MLP) to use one of several potential non-linear activation functions in an elegant way.]]></summary></entry><entry><title type="html">Refactoring a Machine Learning Model</title><link href="https://cthoyt.com/2022/02/06/model-refactoring.html" rel="alternate" type="text/html" title="Refactoring a Machine Learning Model" /><published>2022-02-06T15:45:00+00:00</published><updated>2022-02-06T15:45:00+00:00</updated><id>https://cthoyt.com/2022/02/06/model-refactoring</id><content type="html" xml:base="https://cthoyt.com/2022/02/06/model-refactoring.html"><![CDATA[<p>This blog post is a tutorial that will take you from a naive implementation of a
<a href="https://en.wikipedia.org/wiki/Multilayer_perceptron">multilayer perceptron (MLP)</a>
in PyTorch to an enlightened implementation that simultaneously leverages the
power of <a href="https://pytorch.org">PyTorch</a>, Python’s built-ins, and some powerful
third party Python packages.</p>

<p>This tutorial is going to assume the following imports for all code blocks:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">itertools</span> <span class="k">as</span> <span class="n">itt</span>

<span class="kn">import</span> <span class="nn">more_itertools</span>
<span class="kn">from</span> <span class="nn">torch</span> <span class="kn">import</span> <span class="n">nn</span>
<span class="kn">from</span> <span class="nn">torch.nn</span> <span class="kn">import</span> <span class="n">functional</span> <span class="k">as</span> <span class="n">F</span>
</code></pre></div></div>

<ol>
  <li><a href="https://docs.python.org/3/library/itertools.html"><code class="language-plaintext highlighter-rouge">itertools</code></a> is a builtin
library for helping deal with lists, sets, and other iterables.</li>
  <li><a href="https://github.com/more-itertools/more-itertools"><code class="language-plaintext highlighter-rouge">more_itertools</code></a> is a
third-party extension to itertools, highly regarded in the Python community.</li>
  <li>You should already be familiar with <a href="https://pytorch.org/">PyTorch</a> and
writing your own subclasses of <code class="language-plaintext highlighter-rouge">torch.nn.Module</code> by implementing your own
<code class="language-plaintext highlighter-rouge">__init__()</code> and <code class="language-plaintext highlighter-rouge">forward()</code> functions.</li>
</ol>

<p>This tutorial isn’t really about the theory nor application of machine learning
models - it’s just about the best ways to implement them. I’m also going to
commit the sin of omitting docstrings and a lot of type annotations, since most
of the MLP should be pretty obvious.</p>

<p>Let’s start with a naive implementation, that reflects some old habits from C or
Java programming:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">torch</span>
<span class="kn">from</span> <span class="nn">torch</span> <span class="kn">import</span> <span class="n">nn</span>
<span class="kn">from</span> <span class="nn">torch.nn</span> <span class="kn">import</span> <span class="n">functional</span> <span class="k">as</span> <span class="n">F</span>


<span class="k">class</span> <span class="nc">MLP1</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dims</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="n">layers</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">dims</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">):</span>
            <span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span> <span class="o">=</span> <span class="n">dims</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">dims</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]</span>
            <span class="n">layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">))</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">layers</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">ModuleList</span><span class="p">(</span><span class="n">layers</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">FloatTensor</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">torch</span><span class="p">.</span><span class="n">FloatTensor</span><span class="p">:</span>
        <span class="n">rv</span> <span class="o">=</span> <span class="n">x</span>
        <span class="k">for</span> <span class="n">layer</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">layers</span><span class="p">:</span>
            <span class="n">rv</span> <span class="o">=</span> <span class="n">layer</span><span class="p">(</span><span class="n">rv</span><span class="p">)</span>
            <span class="n">rv</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="n">relu</span><span class="p">(</span><span class="n">rv</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">rv</span>
</code></pre></div></div>

<h2 id="incremental-improvements">Incremental Improvements</h2>

<p>MLP1 uses the dreaded <code class="language-plaintext highlighter-rouge">range(len(...))</code> pattern, which can almost always be
replaced with direct iteration. However, in this case, it uses the index to get
the next element with it. Luckily, <code class="language-plaintext highlighter-rouge">more_itertools</code> has a function
<a href="https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.pairwise"><code class="language-plaintext highlighter-rouge">pairwise()</code></a>
that does exactly this. MLP1 can then be refactored into:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">torch</span>
<span class="kn">from</span> <span class="nn">more_itertools</span> <span class="kn">import</span> <span class="n">pairwise</span>
<span class="kn">from</span> <span class="nn">torch</span> <span class="kn">import</span> <span class="n">nn</span>
<span class="kn">from</span> <span class="nn">torch.nn</span> <span class="kn">import</span> <span class="n">functional</span> <span class="k">as</span> <span class="n">F</span>


<span class="k">class</span> <span class="nc">MLP2</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dims</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="n">layers</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="k">for</span> <span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span> <span class="ow">in</span> <span class="n">pairwise</span><span class="p">(</span><span class="n">dims</span><span class="p">):</span>  <span class="c1"># this line changed
</span>            <span class="n">layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">))</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">layers</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">ModuleList</span><span class="p">(</span><span class="n">layers</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">FloatTensor</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">torch</span><span class="p">.</span><span class="n">FloatTensor</span><span class="p">:</span>
        <span class="n">rv</span> <span class="o">=</span> <span class="n">x</span>
        <span class="k">for</span> <span class="n">layer</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">layers</span><span class="p">:</span>
            <span class="n">rv</span> <span class="o">=</span> <span class="n">layer</span><span class="p">(</span><span class="n">rv</span><span class="p">)</span>
            <span class="n">rv</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="n">relu</span><span class="p">(</span><span class="n">rv</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">rv</span>
</code></pre></div></div>

<p>The application of
<a href="https://pytorch.org/docs/stable/generated/torch.nn.functional.relu.html"><code class="language-plaintext highlighter-rouge">F.relu</code></a>
in <code class="language-plaintext highlighter-rouge">forward()</code> is suspect for a few reasons:</p>

<ol>
  <li>Because it lives as a hard-coded call in <code class="language-plaintext highlighter-rouge">forward()</code>, there’s no way to make
it into a hyper-parameter that can be chosen by a user</li>
  <li>Because it’s the functional form <code class="language-plaintext highlighter-rouge">F.relu</code> and not <code class="language-plaintext highlighter-rouge">nn.ReLU</code>, it can’t be
stacked with other layers</li>
</ol>

<p>MLP2 can be refactored to address both of those by using the modular form
<code class="language-plaintext highlighter-rouge">nn.ReLU</code> in the layers after creating each <code class="language-plaintext highlighter-rouge">nn.Linear</code>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">torch</span>
<span class="kn">from</span> <span class="nn">more_itertools</span> <span class="kn">import</span> <span class="n">pairwise</span>
<span class="kn">from</span> <span class="nn">torch</span> <span class="kn">import</span> <span class="n">nn</span>


<span class="k">class</span> <span class="nc">MLP3</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dims</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="n">layers</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="k">for</span> <span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span> <span class="ow">in</span> <span class="n">pairwise</span><span class="p">(</span><span class="n">dims</span><span class="p">):</span>
            <span class="n">layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">))</span>
            <span class="n">layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">())</span>  <span class="c1"># this line changed
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">layers</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">ModuleList</span><span class="p">(</span><span class="n">layers</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">FloatTensor</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">torch</span><span class="p">.</span><span class="n">FloatTensor</span><span class="p">:</span>
        <span class="n">rv</span> <span class="o">=</span> <span class="n">x</span>
        <span class="k">for</span> <span class="n">layer</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">layers</span><span class="p">:</span>
            <span class="n">rv</span> <span class="o">=</span> <span class="n">layer</span><span class="p">(</span><span class="n">rv</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">rv</span>
</code></pre></div></div>

<p>Now that the <code class="language-plaintext highlighter-rouge">forward()</code> function is just a successive application of layers, it
can be exchanged with a <code class="language-plaintext highlighter-rouge">nn.Sequential</code>. MLP3 can be refactored to look like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">torch</span>
<span class="kn">from</span> <span class="nn">more_itertools</span> <span class="kn">import</span> <span class="n">pairwise</span>
<span class="kn">from</span> <span class="nn">torch</span> <span class="kn">import</span> <span class="n">nn</span>


<span class="k">class</span> <span class="nc">MLP4</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dims</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="n">layers</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="k">for</span> <span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span> <span class="ow">in</span> <span class="n">pairwise</span><span class="p">(</span><span class="n">dims</span><span class="p">):</span>
            <span class="n">layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">))</span>
            <span class="n">layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">())</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">layers</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span><span class="o">*</span><span class="n">layers</span><span class="p">)</span>  <span class="c1"># this line changed
</span>
    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">FloatTensor</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">torch</span><span class="p">.</span><span class="n">FloatTensor</span><span class="p">:</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">layers</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
</code></pre></div></div>

<p>The following two improvements will make the construction of the <code class="language-plaintext highlighter-rouge">layers</code> list
that goes in <code class="language-plaintext highlighter-rouge">nn.Sequential</code> much more elegant. First, we’ll refactor MLP4 to
use the <code class="language-plaintext highlighter-rouge">extend</code> function of a list rather than <code class="language-plaintext highlighter-rouge">append</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">torch</span>
<span class="kn">from</span> <span class="nn">more_itertools</span> <span class="kn">import</span> <span class="n">pairwise</span>
<span class="kn">from</span> <span class="nn">torch</span> <span class="kn">import</span> <span class="n">nn</span>


<span class="k">class</span> <span class="nc">MLP5</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dims</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="n">layers</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="k">for</span> <span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span> <span class="ow">in</span> <span class="n">pairwise</span><span class="p">(</span><span class="n">dims</span><span class="p">):</span>
            <span class="n">layers</span><span class="p">.</span><span class="n">extend</span><span class="p">((</span>
                <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">),</span>
                <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">(),</span>
            <span class="p">))</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">layers</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span><span class="o">*</span><span class="n">layers</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">FloatTensor</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">torch</span><span class="p">.</span><span class="n">FloatTensor</span><span class="p">:</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">layers</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="an-aside-on-list-comprehensions">An Aside on List Comprehensions</h2>

<p>As we prepare to refactor MLP5, we’ll take a short aside to discuss list
comprehensions in Python. Here are a few resources to get you started:</p>

<ol>
  <li><a href="https://www.youtube.com/watch?v=EnSu9hHGq5o&amp;list=PLPFmTfhIBiumfYT3rsa35fHJxabB78er1&amp;index=4&amp;t=1s">Ned Batchelder - Loop like a native: while, for, iterators, generators</a></li>
  <li><a href="https://www.youtube.com/watch?v=ei71YpmfRX4&amp;list=PLPFmTfhIBiumfYT3rsa35fHJxabB78er1&amp;index=6">Trey Hunner - Comprehensible Comprehensions</a></li>
</ol>

<p>The minimum amount of information you need to know for this tutorial is that
anytime we see code that looks like</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">old_list</span> <span class="o">=</span> <span class="p">...</span>
<span class="n">new_list</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">old_list</span><span class="p">:</span>
    <span class="n">new_list</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">transform</span><span class="p">(</span><span class="n">x</span><span class="p">))</span>
</code></pre></div></div>

<p>we know that we can transform it using a list comprehension like</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">old_list</span> <span class="o">=</span> <span class="p">...</span>
<span class="n">new_list</span> <span class="o">=</span> <span class="p">[</span>
    <span class="n">transform</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">old_list</span>
<span class="p">]</span>
</code></pre></div></div>

<p>There’s an analogous pattern for when we’re successively extending a list, like
what we did when writing MLP5. If we see code that looks like</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">old_list</span> <span class="o">=</span> <span class="p">...</span>
<span class="n">new_list</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">old_list</span><span class="p">:</span>
    <span class="n">new_list</span><span class="p">.</span><span class="n">extend</span><span class="p">(</span><span class="n">transform</span><span class="p">(</span><span class="n">x</span><span class="p">))</span>
</code></pre></div></div>

<p>we can transform it into something more elegant using
<a href="https://docs.python.org/3/library/itertools.html#itertools.chain.from_iterable"><code class="language-plaintext highlighter-rouge">itertools.chain.from_iterable()</code></a>
like</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">itertools</span> <span class="kn">import</span> <span class="n">chain</span>

<span class="n">old_list</span> <span class="o">=</span> <span class="p">...</span>
<span class="n">new_list</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">chain</span><span class="p">.</span><span class="n">from_iterable</span><span class="p">(</span>
    <span class="n">transform</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">old_list</span>
<span class="p">))</span>
</code></pre></div></div>

<p>While this may be a few extra lines (because it’s broken up for readability), it
has the advantage that it’s only one <em>logical line</em> and can be used in more
clever ways.</p>

<h2 id="bringing-it-all-together">Bringing it All Together</h2>

<p>We’ll apply this template to our code to get a one-liner for instantiating our
<code class="language-plaintext highlighter-rouge">nn.Sequential</code> (though notice it’s again broken up onto multiple lines for
readability):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">itertools</span> <span class="kn">import</span> <span class="n">chain</span>

<span class="kn">import</span> <span class="nn">torch</span>
<span class="kn">from</span> <span class="nn">more_itertools</span> <span class="kn">import</span> <span class="n">pairwise</span>
<span class="kn">from</span> <span class="nn">torch</span> <span class="kn">import</span> <span class="n">nn</span>


<span class="k">class</span> <span class="nc">MLP6</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dims</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">layers</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span><span class="o">*</span><span class="n">chain</span><span class="p">.</span><span class="n">from_iterable</span><span class="p">(</span>
            <span class="p">(</span>
                <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">),</span>
                <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">(),</span>
            <span class="p">)</span>
            <span class="k">for</span> <span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span> <span class="ow">in</span> <span class="n">pairwise</span><span class="p">(</span><span class="n">dims</span><span class="p">)</span>
        <span class="p">))</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">FloatTensor</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">torch</span><span class="p">.</span><span class="n">FloatTensor</span><span class="p">:</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">layers</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
</code></pre></div></div>

<p>Finally, since we’re now just creating a module that wraps the exact
functionality of <code class="language-plaintext highlighter-rouge">nn.Sequential</code>, it’s possible to directly subclass
<code class="language-plaintext highlighter-rouge">nn.Sequential</code>. We’ll refactor on MLP6 to get our final result:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">itertools</span> <span class="kn">import</span> <span class="n">chain</span>

<span class="kn">from</span> <span class="nn">more_itertools</span> <span class="kn">import</span> <span class="n">pairwise</span>
<span class="kn">from</span> <span class="nn">torch</span> <span class="kn">import</span> <span class="n">nn</span>


<span class="k">class</span> <span class="nc">MLP7</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dims</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="o">*</span><span class="n">chain</span><span class="p">.</span><span class="n">from_iterable</span><span class="p">(</span>
            <span class="p">(</span>
                <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">),</span>
                <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">(),</span>
            <span class="p">)</span>
            <span class="k">for</span> <span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span> <span class="ow">in</span> <span class="n">pairwise</span><span class="p">(</span><span class="n">dims</span><span class="p">)</span>
        <span class="p">))</span>
</code></pre></div></div>

<p>MLP7 is now a much more simple implementation that uses a few neat tricks to
reduce error-prone logic. I hope you enjoy applying these patterns to your own
models, and if you have any other ideas you’d like me to include here, please
leave comment or get in touch!</p>

<hr />

<p>While we were originally aiming at reducing complexity, this model still has the
issue that it contains a hard-coded reference to the ReLU non-linear activation
function, which could be easily generalized to support alternate non-linear
activation functions. In my <a href="/2022/02/06/model-abstraction.html">next
post</a>, I’ll demonstrate the thought
process behind this and the ultimate solution using the
<a href="https://github.com/cthoyt/class-resolver"><code class="language-plaintext highlighter-rouge">class-resolver</code></a>.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="programming" /><category term="machine learning" /><category term="deep learning" /><category term="Python" /><summary type="html"><![CDATA[This blog post is a tutorial that will take you from a naive implementation of a multilayer perceptron (MLP) in PyTorch to an enlightened implementation that simultaneously leverages the power of PyTorch, Python’s built-ins, and some powerful third party Python packages.]]></summary></entry><entry><title type="html">The Official Rules of Python Packaging Speedrunning</title><link href="https://cthoyt.com/2022/01/24/packaging-speedrunning.html" rel="alternate" type="text/html" title="The Official Rules of Python Packaging Speedrunning" /><published>2022-01-24T00:30:00+00:00</published><updated>2022-01-24T00:30:00+00:00</updated><id>https://cthoyt.com/2022/01/24/packaging-speedrunning</id><content type="html" xml:base="https://cthoyt.com/2022/01/24/packaging-speedrunning.html"><![CDATA[<p>I figured over the holiday break or early days of the new year, I’d catch up on
some serious blogging. Instead, here’s my first post of 2022: a silly take on a
topic I actually care a lot about. Here are the rules for Python Packaging
Speedruns.</p>

<h2 id="whats-a-speedrun">What’s a speedrun?</h2>

<p>Speedrunning is the sport of finishing a video game as fast as possible. Some
games are so broken (e.g.,
<a href="https://www.speedrun.com/oot">The Legend of Zelda: Ocarina of Time</a>) that it’s
not really even that interesting to just get to the end, so many games introduce
additional variants to Any% such as 100%. For each game, the speedrunning
community decides when the timer should start (e.g., most games have a start
screen and this is usually pretty obvious) and when the timer should end (e.g.,
when the credits screen rolls or when the last user input is given on a final
boss).</p>

<h2 id="whats-a-packaging-speedrun">What’s a packaging speedrun?</h2>

<p>The video game speedrunning community has the benefit that the same speedrun can
be done over and over again, with pretty reliable conditions on how it starts
and end. However, each Python packaging speedrun will be done on a different
repository. It doesn’t really make sense for more than one person to do a
packaging speedrun on the same repository, since the goal besides being fast is
to make practical improvements to unpackaged code and submit a pull request to
original authors.</p>

<h2 id="python-packaging-speedrun-any">Python Packaging Speedrun Any%</h2>

<p>I’m going to propose a set of minimal rules for a Python packaging speedrun,
which I’ll designate as Any% because it’s the simplest version that has the
least restrictions. Other categories can take these rules and build on top of
them.</p>

<h3 id="rule-1-timing">Rule 1: Timing</h3>

<p>Python packaging speedruns are timed in the following way:</p>

<ol>
  <li>Start the clock when you click the fork button on GitHub</li>
  <li>End the clock after all commits have been made to your fork AND a pull
request has been made.</li>
</ol>

<h3 id="rule-2-package-layout">Rule 2: Package Layout</h3>

<p>To ensure imports aren’t sneakily being done via directory structure, the <code class="language-plaintext highlighter-rouge">src/</code>
layout is mandatory for <em>all</em> Python packaging speedruns. Read Hynek Schlawack’s
excellent explanation why this restriction is necessary
<a href="https://hynek.me/articles/testing-packaging/">here</a>.</p>

<h3 id="rule-3-minimal-unit-testing">Rule 3: Minimal Unit Testing</h3>

<p>To make sure that packaging was done properly, automated unit tests should check
that the package can be imported using the same directory structure where
<code class="language-plaintext highlighter-rouge">tests/test_trivial.py</code> contains the following:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># test_trivial.py
</span>
<span class="kn">import</span> <span class="nn">importlib</span>
<span class="kn">import</span> <span class="nn">unittest</span>

<span class="k">class</span> <span class="nc">TestTrivial</span><span class="p">(</span><span class="n">unittest</span><span class="p">.</span><span class="n">TestCase</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">test_import</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">name</span> <span class="o">=</span> <span class="s">"&lt;your package name goes here&gt;"</span>
        <span class="n">module</span> <span class="o">=</span> <span class="n">importlib</span><span class="p">.</span><span class="n">import_module</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">assertIsNotNone</span><span class="p">(</span><span class="n">module</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="rule-4-automated-application-of-testing">Rule 4: Automated Application of Testing</h3>

<p>This file should be run with <code class="language-plaintext highlighter-rouge">tox</code> on a <code class="language-plaintext highlighter-rouge">tox.ini</code> that minimally contains the
following <code class="language-plaintext highlighter-rouge">[testenv]</code>:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># tox.ini
</span>
<span class="nn">[tox]</span>
<span class="py">envlist</span> <span class="p">=</span>
    <span class="err">py</span>

<span class="nn">[testenv]</span>
<span class="py">commands</span> <span class="p">=</span>
    <span class="err">pytest</span> <span class="err">tests/</span>
<span class="py">deps</span> <span class="p">=</span>
    <span class="err">pytest</span>
<span class="py">description</span> <span class="p">=</span> <span class="s">Run unit tests.</span>
</code></pre></div></div>

<p>Note the <code class="language-plaintext highlighter-rouge">[tox]</code> section was included for ergonomic usage of the <code class="language-plaintext highlighter-rouge">tox</code> command
from the command line.</p>

<h3 id="rule-5-provenance">Rule 5: Provenance</h3>

<p>Finally, like video game speedruns, there needs to be video proof, preferably
uploaded to YouTube or witnessed on Twitch.</p>

<h2 id="potential-variants">Potential Variants</h2>

<p>Since the Any% is pretty simple, I think there is room for all sorts of variants
including:</p>

<ol>
  <li>Passes flake8 with a pre-determined set of plugins (e.g., flake8-black,
flake8-isort, flake8-docstrings, pydocstyle)</li>
  <li>Passes flake8 with previous plugins and harder ones (e.g., flake8-print,
darglint)</li>
  <li>Passes mypy</li>
</ol>

<h2 id="parting-thoughts">Parting thoughts</h2>

<p>Each speedrun will require a different amount of effort to achieve these things
based on the size of the package and what state the original code was in.
Obviously, Python packaging speedruns can’t be so rigorously compared as video
game speedruns. That’s okay.</p>

<p>I posted my
<a href="https://www.youtube.com/watch?v=-aje6kszNcc">first Python packaging speedrunning</a>
on YouTube this evening. It doesn’t actually follow the rules I proposed here
because I started thinking about this after I was done
<a href="https://twitter.com/cthoyt/status/1485406393251377159">tweeting</a> about it.
Please let me know if you have any ideas on how to improve these rules, have an
idea for a new category, or if you want me to link to your Python packaging
speedrun video.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="programming" /><summary type="html"><![CDATA[I figured over the holiday break or early days of the new year, I’d catch up on some serious blogging. Instead, here’s my first post of 2022: a silly take on a topic I actually care a lot about. Here are the rules for Python Packaging Speedruns.]]></summary></entry><entry><title type="html">How to Pick a Unique Prefix</title><link href="https://cthoyt.com/2021/12/17/unique-prefix-picking.html" rel="alternate" type="text/html" title="How to Pick a Unique Prefix" /><published>2021-12-17T13:17:00+00:00</published><updated>2021-12-17T13:17:00+00:00</updated><id>https://cthoyt.com/2021/12/17/unique-prefix-picking</id><content type="html" xml:base="https://cthoyt.com/2021/12/17/unique-prefix-picking.html"><![CDATA[<p>After the
<a href="https://github.com/OBOFoundry/OBOFoundry.github.io/pull/1703">recent incident</a>
on the OBO Foundry where an inexperienced group submitted a new ontology request
using a prefix that already existed in the
<a href="https://bioportal.bioontology.org">BioPortal</a>, there has been a
<a href="https://github.com/OBOFoundry/OBOFoundry.github.io/issues/1704">renewed interest</a>
in implementing an automated solution to protect against this.</p>

<h2 id="the-big-picture">The Big Picture</h2>

<p>A more general issue is that there can be prefix conflicts between different
registries. I found a few examples of this happening while building the
<a href="https://bioregistry.io">Bioregistry</a> in the Spring 2021. Notably, this included
the conflict between the
<a href="https://github.com/ufbmi/geographical-entity-ontology">Geographical Entity Ontology</a>
and the <a href="https://www.ncbi.nlm.nih.gov/geo/">Gene Expression Omnibus</a> which both
used the prefix <code class="language-plaintext highlighter-rouge">geo</code> in the <a href="https://obofoundry.org/ontology/geo">OBO Foundry</a>
and <a href="https://registry.identifiers.org/registry/geo">Identifiers.org</a>,
respectively. These conflicts needed thoughtful mediation. After
<a href="https://github.com/ufbmi/geographical-entity-ontology/issues/19">discussing on GitHub</a>
with <a href="https://github.com/hoganwr">Bill Hogan</a>, the responsible author of the
Geographical Entity Ontology, we decided that to use the prefix
<a href="https://bioregistry.io/registry/geogeo"><code class="language-plaintext highlighter-rouge">geogeo</code></a> in the Bioregistry. The Gene
Expression Omnibus maintained its usage of
<a href="https://bioregistry.io/registry/geo"><code class="language-plaintext highlighter-rouge">geo</code></a> due to its much wider usage and
longer history.</p>

<p>As a follow-up, I began curating a
<a href="https://github.com/biopragmatics/bioregistry/blob/main/src/bioregistry/data/mismatch.json">list of conflicts</a>
and
<a href="https://github.com/biopragmatics/bioregistry/pull/62">implemented a technical solution</a>
in the Bioregistry’s nightly automated alignment workflow to prevent automated
alignment between known conflicting prefixes from different registries, but this
only a partial solution to a problem that is ultimately dependent on having
confidence in the external resources (which is indeed its own issue).</p>

<h2 id="what-the-bioregistry-aligns">What the Bioregistry Aligns</h2>

<p>A small, select set of registries are fully automatically ingested in the
Bioregistry which includes (at the time of writing) Identifiers.org,
Name-to-Thing, the OBO Foundry, and the Ontology Lookup Service. The remaining
registries are excluded for a variety of reasons including redundancy with other
resources (e.g., AberOWL and OntoBee), a lack of modernization or alignment
(e.g., NCBI’s registry), general inclusion of non-nomenclature resources (e.g.,
UniProt’s registry, FAIRsharing), and a lack of minimum quality standards (e.g.,
BioPortal). A summary and slightly more detailed explanation about these sources
can be found <a href="https://bioregistry.io/summary">here</a>.</p>

<p>In many ways, the fact that the Bioregistry fully imports some resources and
automatically aligns with others</p>

<p>The Bioregistry imports Identifiers.org, OBO Foundry, and N2T as well as many
other resources (see for a full list), so it can be a one-stop shop for most
resources. However, it does not import all of BioPortal, so users should check
there too.</p>

<h2 id="how-to-check-your-prefix-is-unique">How to Check Your Prefix is Unique</h2>

<p>Ultimately the point of this post is to present a workflow for any potential who
want to check their new ontology request has a unique prefix (which will soon be
a technical requirement in the OBO Foundry). Because the Bioregistry imports
many resources, it’s sufficient to just check the Bioregistry and BioPortal
(assuming you’re interested in respecting the BioPortal content).</p>

<h3 id="manual-check">Manual Check</h3>

<p>The first way to check if your prefix is unique is to manually read through some
of the sites.</p>

<table>
  <thead>
    <tr>
      <th>Resource</th>
      <th>Home Page</th>
      <th>Prefix List</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Bioregistry</td>
      <td>https://bioregistry.io</td>
      <td>https://bioregistry.io/registry</td>
    </tr>
    <tr>
      <td>Bioportal</td>
      <td>https://bioportal.bioontology.org</td>
      <td>https://bioportal.bioontology.org/ontologies</td>
    </tr>
  </tbody>
</table>

<p>While the BioPortal API is locked behind API key access, the Bioregistry
additionally has a search endpoint at <code class="language-plaintext highlighter-rouge">https://bioregistry.io/api/search?q=...</code></p>

<h3 id="data-dumps">Data Dumps</h3>

<p>The second way to check if your prefix is unique is by comparing it to full
dumps of the Bioregistry and BioPortal. The Bioregistry can be downloaded in
several formats that are updated on a nightly basis:</p>

<ul>
  <li><a href="https://github.com/biopragmatics/bioregistry/blob/main/exports/registry/registry.tsv">TSV</a></li>
  <li><a href="https://github.com/biopragmatics/bioregistry/blob/main/exports/registry/registry.json">JSON</a></li>
  <li><a href="https://github.com/biopragmatics/bioregistry/blob/main/exports/registry/registry.yml">YAML</a></li>
  <li><a href="https://github.com/biopragmatics/bioregistry/tree/main/exports/rdf">RDF</a></li>
</ul>

<p>BioPortal doesn’t offer any first-party data dumps, but the Bioregistry
generates one nightly
<a href="https://github.com/biopragmatics/bioregistry/blob/main/src/bioregistry/data/external/bioportal/raw.json">here</a></p>

<h3 id="programmatic-access">Programmatic Access</h3>

<p>The third way to check if your prefix is unique is by comparing it to the
Bioregistry and BioPortal using code from the <code class="language-plaintext highlighter-rouge">bioregistry</code> python package
(which is updated nightly).</p>

<p>Programmatic way to check if something is in the Bioregistry:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">bioregistry</span>

<span class="n">query</span> <span class="o">=</span> <span class="s">"EPSO"</span>
<span class="n">available_in_bioregistry</span> <span class="o">=</span> <span class="n">bioregistry</span><span class="p">.</span><span class="n">normalize_prefix</span><span class="p">(</span><span class="n">query</span><span class="p">)</span> <span class="ow">is</span> <span class="bp">None</span>
</code></pre></div></div>

<p>Programmatic way to check if something is in BioPortal:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">bioregistry.external.bioportal</span> <span class="kn">import</span> <span class="n">get_bioportal</span>

<span class="n">query</span> <span class="o">=</span> <span class="s">"EPSO"</span>
<span class="n">bioportal_dict</span> <span class="o">=</span> <span class="n">get_bioportal</span><span class="p">()</span>
<span class="n">available_in_bioportal</span> <span class="o">=</span> <span class="n">query</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">bioportal_dict</span>
</code></pre></div></div>

<hr />

<p>Being high quality and enabling external contribution and improvements are core
to the philosophy of the Bioregistry. While no solution is perfect for listing
all possible prefixes and it might be necessary to do a bit of extra googling
before picking a prefix, this is a great place to start. If during the process
of choosing a prefix you find you might create a conflict, please consider also
<a href="https://github.com/biopragmatics/bioregistry/issues/new/choose">suggesting a new entry</a>
in the Bioregistry.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="semantics" /><summary type="html"><![CDATA[After the recent incident on the OBO Foundry where an inexperienced group submitted a new ontology request using a prefix that already existed in the BioPortal, there has been a renewed interest in implementing an automated solution to protect against this.]]></summary></entry><entry><title type="html">A Glossary for the Bioregistry and Biopragmatics Stack</title><link href="https://cthoyt.com/2021/10/07/biopragmatics-glossary.html" rel="alternate" type="text/html" title="A Glossary for the Bioregistry and Biopragmatics Stack" /><published>2021-10-07T13:26:00+00:00</published><updated>2021-10-07T13:26:00+00:00</updated><id>https://cthoyt.com/2021/10/07/biopragmatics-glossary</id><content type="html" xml:base="https://cthoyt.com/2021/10/07/biopragmatics-glossary.html"><![CDATA[<p>There are a lot of terms that I’ve been throwing around when talking about the
Bioregistry, so this blog post is a first draft of a gloassary of all of them.</p>

<p>Later, I will revise this further and put it either on the Bioregistry website,
or make a totally new repo on the
<a href="https://github.com/biopragmatics">Biopragmatics</a> GitHub organization.</p>

<h2 id="semantic-spaces">Semantic spaces</h2>

<p>While a
<a href="https://en.wikipedia.org/wiki/Controlled_vocabulary">controlled vocabulary</a>
enumerates a set of named entities, a <strong>semantic space</strong> enumerates a set of
stable <strong>local identifies</strong> for entities. Most high-quality controlled
vocabularies also assign local identifiers for their named entities and are also
semantic spaces. For example, the
<a href="https://www.ebi.ac.uk/chebi">Chemical Entities of Biological Interest (ChEBI)</a>
is a well-known ontology in the biomedical domain that is both a controlled
vocabulary and a semantic space.</p>

<p>The term <em>local identifier</em> is synonymous with <em>identifier</em> and <em>accession</em>, but
has the added qualifier <em>local</em> as a reminder that two semantic spaces may use
the same one. For example, the
<a href="https://www.ebi.ac.uk/chebi">Chemical Entities of Biological Interest (ChEBI)</a>
entry for
<a href="https://www.ebi.ac.uk/chebi/searchId.do?chebiId=1234">6-methoxy-2-octaprenyl-1,4-benzoquinone</a>
and the <a href="https://bioregistry.io/doid">Human Disease Ontology (DOID)</a> entry for
<a href="https://www.ebi.ac.uk/ols/ontologies/doid/terms?obo_id=DOID:1234">gender identity disorder</a>
share the local identifier of <code class="language-plaintext highlighter-rouge">1234</code>.</p>

<h3 id="formalizing-local-identifiers">Formalizing local identifiers</h3>

<p>It’s often useful to have a
<a href="https://en.wikipedia.org/wiki/Regular_expression">regular expression</a> that
describes local identifiers of a given semantic space. For example, both ChEBI
and DOID use local identifiers that look like numbers, which match the regular
expression <code class="language-plaintext highlighter-rouge">^\d+$</code>. The <code class="language-plaintext highlighter-rouge">^</code> and <code class="language-plaintext highlighter-rouge">$</code> denote the beginning and end of the regular
expression and appear exactly the same in all regular expressions for local
identifiers. The <code class="language-plaintext highlighter-rouge">\d</code> will match a number and the <code class="language-plaintext highlighter-rouge">+</code> means that the preceding
token (<code class="language-plaintext highlighter-rouge">\d</code>) can be matched one or more times in a row.</p>

<p>It’s important to remember that identifiers might look like numbers, but they
should <em>never</em> be treated as such. For example, the
<a href="https://bioregistry.io/go">Gene Ontology (GO)</a> uses identifiers that are
left-padded with zeros like in <code class="language-plaintext highlighter-rouge">0032571</code> for
<a href="https://bioregistry.io/go:0032571">response to vitamin K</a>. The regular
expression pattern for GO entries is <code class="language-plaintext highlighter-rouge">^\d{7}$</code>, since there are always exactly
seven numbers. Regular expressions don’t have a straightforward way to describe
numbers that are left padded with zero, so keep in mind that this is
approximation is a good balance between precision and simplicity.</p>

<p>There are a variety of patterns used for identifiers, including integers
(<code class="language-plaintext highlighter-rouge">^\d+$</code>; e.g., PubMed), zero padded integers (<code class="language-plaintext highlighter-rouge">^\d{7}$</code>; e.g., GO and other OBO
Ontologies), universally unique identifiers (UUIDs; e.g., NCI Pathway
Interaction Database, NDEx), and many other variations.</p>

<h3 id="origins">Origins</h3>

<p>Semantic spaces arise from several kinds of resources such as:</p>

<ol>
  <li><strong>Ontologies</strong> like the <a href="https://bioregistry.io/go">Gene Ontology (GO)</a>,
<a href="https://bioregisty.io/chebi">Chemical Entities of Biological Interest (ChEBI)</a>,
and <a href="https://bioregistry.io/efo">Experimental Factor Ontology (EFO)</a></li>
  <li><strong>Controlled Vocabularies</strong> like
<a href="https://bioregistry.io/ncbigene">Entrez Gene</a>,
<a href="https://biorestry.io/interpro">InterPro</a>, and
<a href="https://bioregistry.io/fplx">FamPlex</a></li>
  <li><strong>Databases</strong> like <a href="https://bioregistry.io/pdb">Protein Data Bank</a> and
<a href="https://bioregistry.io/geo">Gene Expression Omnibus</a></li>
</ol>

<h3 id="completeness">Completeness</h3>

<p>Semantic spaces typically fall into one of several “completeness” categories:</p>

<ol>
  <li><strong>Complete by Definition</strong> like
<a href="https://bioregistry.io/eccode">Enzyme Classification</a></li>
  <li><strong>Complete, but Subject to Change</strong> like <a href="https://bioregistry.io/hgnc">HGNC</a></li>
  <li><strong>Always Incomplete</strong> like
<a href="https://bioregisty.io/chebi">Chemical Entities of Biological Interest (ChEBI)</a>
and the <a href="https://bioregistry.io/pdb">Protein Data Bank (PDB)</a></li>
</ol>

<h3 id="scope">Scope</h3>

<p>Semantic spaces have a variety of scopes:</p>

<ol>
  <li><strong>Single entity type</strong> like <a href="https://bioregistry.io/hgnc">HGNC</a></li>
  <li><strong>A few entity types</strong> like the
<a href="https://bioregistry.io/go">Gene Ontology (GO)</a></li>
  <li><strong>Many entity types</strong> like
<a href="https://bioregistry.io/mesh">Medical Subject Headings (MeSH)</a>,
<a href="https://bioregistry.io/ums">Unified Medical Language System (UMLS)</a>,
<a href="https://bioregistry.io/ncit">National Cancer Institute Thesaurus (NCIT)</a></li>
</ol>

<h3 id="relationship-to-projects-and-organizations">Relationship to Projects and Organizations</h3>

<p>Semantic spaces do not always correspond one-to-one with projects, such as how
the ChEMBL database contains both the
<a href="https://bioregistry.io/chembl.compound">ChEMBL Compound</a> and
<a href="https://bioregistry.io/chembl.target">ChEMBL Target</a> semantic spaces or how the
Uber Anatomy Ontology (UBERON) contains both
<a href="https://bioregistry.io/uberon">UBERON</a> and UBPROP semantic spaces for terms and
properties, respectively.</p>

<h2 id="providers">Providers</h2>

<p>A provider returns information about entities from a given semantic space. A
provider is characterized by a <strong>URI format string</strong>, or URI formatter, into
which a local identifier from its semantic space can be substituted for a
special token (e.g., <code class="language-plaintext highlighter-rouge">$1</code>). For example, the following formatter can be used to
get a web page about <a href="https://bioregistry.io/hgnc:5173">HRAS</a> by replacing <code class="language-plaintext highlighter-rouge">$1</code>
in the URI format string
<code class="language-plaintext highlighter-rouge">http://www.genenames.org/cgi-bin/gene_symbol_report?hgnc_id=$1</code> by its HGNC
identifier, <code class="language-plaintext highlighter-rouge">5173</code>.</p>

<p>Well-behaved URI format strings only have one instance of the special token that
occurs at the end. Poorly-behaved URI format strings may have additional
characters following the special token as in
<code class="language-plaintext highlighter-rouge">http://rebase.neb.com/rebase/enz/$1.html</code> for
<a href="https://bioregistry.io/rebase">REBASE</a> or as in
<code class="language-plaintext highlighter-rouge">http://eawag-bbd.ethz.ch/$1/$1_map.html</code> for the
<a href="http://bioregistry.io/umbbd.pathway">UM-BBD Pathway database</a>.</p>

<h3 id="content-type">Content Type</h3>

<p>While providers typically return human-readable HTML, they can also return many
other data types, including:</p>

<ul>
  <li>Images (e.g.,
https://www.ebi.ac.uk/chebi/displayImage.do?defaultImage=true&amp;chebiId=132964
for the ChEBI entry on fluazifop-P-butyl)</li>
  <li>XML (e.g., https://www.uniprot.org/uniprot/P10636.xml for UniProt entry on
human Microtubule-associated protein tau)</li>
  <li>JSON (e.g.,
https://gen3.biodatacatalyst.nhlbi.nih.gov/ga4gh/drs/v1/objects/0000ffeb-36e0-4a29-b21d-84423bda979d
for NCBI’s BioData Catalyst)</li>
  <li>RDF</li>
</ul>

<p>Providers can return any other information that can be transferred via HTTP,
FTP, or related data transfer protocols. Alternatively, content negotiation
could be used to return multiple kinds of data from the same provider URI.</p>

<h3 id="responsibility">Responsibility</h3>

<p>Most semantic spaces have an associated first-party provider that returns
information via a web page. Some semantic spaces, like ChEBI, have several
first-party providers for different content types (e.g., HTML, image). Some
semantic spaces, like Entrez Gene, have additional external providers, including
databases that use its identifiers like the Comparative Toxicogenomics Database.
Some semantic spaces, such as many OBO ontologies, do not have an associated
first party provider and rely solely on third party browsers like AberOWL,
OntoBee, and the Ontology Lookup Service.</p>

<h2 id="naming-things-on-the-semantic-web">Naming things on the semantic web</h2>

<p>There are two (mostly) interchangeable formalisms for naming things in the
semantic web: uniform resource identifiers (URIs) and compact uniform resource
identifiers (CURIEs).</p>

<h3 id="uniform-resource-identifiers-uris">Uniform Resource Identifiers (URIs)</h3>

<p>The semantic web community has adopted the <strong>internationalized resource
identifier (IRI)</strong> as the <em>de facto</em> standard for naming entities. In practice,
usage is often restricted to IRIs that are also <strong>uniform resource identifiers
(URIs)</strong> (i.e., they only use ASCII characters) and that are also <strong>valid
uniform resource locators (URLs)</strong> (i.e., they point to a web page). In applied
semantic web contexts like biomedicine, the subtleties between URLs, URIs, and
IRIs are disregarded and the term URI is preferred such as in the seminal paper
<a href="https://journals.plos.org/plosbiology/article?id=10.1371/journal.pbio.2001414#sec001">Identifiers for the 21st Century</a>.
A more detailed explanation on the difference between URLs, URIs, and IRIs can
be found
<a href="https://fusion.cs.uni-jena.de/fusion/2016/11/18/iri-uri-url-urn-and-their-differences/">here</a>.</p>

<p>For a given semantic space like ChEBI, URIs can usually be constructed given two
parts:</p>

<ol>
  <li>A <strong>URI prefix</strong> (in red)</li>
  <li>A local identifier (in orange)</li>
</ol>

<p>All URIs from the same semantic space have the same URI prefix (in red), but a
different local identifier (in orange). Here’s an example, using the ChEBI local
identifier for
<a href="https://www.ebi.ac.uk/chebi/searchId.do?chebiId=138488">alsterpaullone</a>:</p>

<p><span style="color:red">https://www.ebi.ac.uk/chebi/searchId.do?chebiId=</span><span style="color:orange">138488</span></p>

<p>There may be potentially many URI prefixes corresponding to the same semantic
space and therefore many URIs describing the same entity. For example, ChEBI
also serves images with:</p>

<p><span style="color:red">https://www.ebi.ac.uk/chebi/displayImage.do?defaultImage=true&amp;imageIndex=0&amp;chebiId=</span><span style="color:orange">138488</span></p>

<h3 id="compact-uniform-resource-identifiers-curies">Compact Uniform Resource Identifiers (CURIEs)</h3>

<p>A <strong>compact uniform resource identifier (CURIE)</strong> allows for the replacement of
a URI prefix in a URI with a short prefix. As a short recapitulation of the
<a href="https://www.w3.org/TR/2010/NOTE-curie-20101216">W3C specification</a>, a CURIE has
three parts:</p>

<ol>
  <li>A prefix (in red)</li>
  <li>A delimiter (in black)</li>
  <li>A local identifier from the given semantic space (in orange)</li>
</ol>

<p>Since everyone agrees on what ChEBI is within the biomedical domain, it makes
sense to use <code class="language-plaintext highlighter-rouge">chebi</code> as the prefix for ChEBI local identifiers. However, there
is no globally unique set of prefixes used across the semantic web (nor should
there be). Therefore, when using CURIEs, you need at minimum a prefix map
(described below) and ideally a registry that stores additional metadata about
each prefix.</p>

<p>Here’s the same example as in the URI section above for alsterpaullone, but now
condensed into a CURIE:</p>

<p><span style="color:red">chebi</span><b>:</b><span style="color:orange">138488</span></p>

<h3 id="converting-between-uris-and-curies">Converting between URIs and CURIEs</h3>

<p>A <strong>prefix map</strong> associates each prefix to exactly one URI prefix. It can be
used to expand CURIEs into URIs. Disregarding (for now) how to choose the best
URI prefix, one potential prefix map that could be used to expand the example
CURIE for alsterpaullone could be:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"chebi"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://www.ebi.ac.uk/chebi/searchId.do?chebiId="</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>A simple algorithm for expanding a CURIE to a URI is as follows:</p>

<ol>
  <li>Split the CURIE on the first instance of the delimiter, usually a colon <code class="language-plaintext highlighter-rouge">:</code></li>
  <li>Look up the left-hand side of the split (i.e., the prefix) in the prefix map</li>
  <li>String concatenate the resulting URI prefix with the right-hand side of the
split (i.e., the local identifier)</li>
</ol>

<p>A <strong>reverse prefix map</strong> associates one or more URI prefixes to each prefix. It
can be used to contract URIs into CURIEs. Disregarding (for now) how to chose
the best prefix for each URI prefix, one potential reverse prefix map that could
be used to contract the two example URIs for alterpaullone could be:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"https://www.ebi.ac.uk/chebi/searchId.do?chebiId="</span><span class="p">:</span><span class="w"> </span><span class="s2">"chebi"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"https://www.ebi.ac.uk/chebi/displayImage.do?defaultImage=true&amp;imageIndex=0&amp;chebiId="</span><span class="p">:</span><span class="w"> </span><span class="s2">"chebi"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Because it’s possible some URI prefixes might overlap, it’s a good heuristic to
check a given URI against a reverse prefix map in decreasing order by URI prefix
length.</p>

<h3 id="poorly-behaved-uris">Poorly Behaved URIs</h3>

<p>Unfortunately, not all URLs that provide information about entities in semantic
spaces can be trivially split into a URI prefix and a local identifier. For
example, the <a href="https://bioregistry.io/rebase">REBASE</a> entry for
<a href="https://bioregistry.io/rebase:101">Asp14HI</a> has the URI
http://rebase.neb.com/rebase/enz/101.html. Note the pesky <code class="language-plaintext highlighter-rouge">.html</code> at the end,
which if removed, causes an HTTP 404 error due to the implementation of the
REBASE website.</p>

<p>While this creates a big problem for parsing URIs into CURIEs, it’s still
possible to generate a URI from a CURIE given a slight variation on a prefix
map, which relies on the previously described notion of URI formatters (see the
section above on Providers)</p>

<p>A URI prefix corresponds to a special case of a URI formatter where there is
exactly one instance of <code class="language-plaintext highlighter-rouge">$1</code> that appears at the end of the string. Therefore,
it is more valuable to curate URI formatters and programmatically generate
prefix maps when possible. The fact that some URIs are hard to construct easily
is also one of the motivations for resolver services, described in a later
section.</p>

<h3 id="open-biomedical-ontologies-curies">Open Biomedical Ontologies CURIEs</h3>

<p>The <a href="http://www.obofoundry.org/">Open Biomedical Ontologies (OBO) Foundry</a>
provides a persistent URL service (PURL) to create stable URIs for biomedical
entities curated in their ontologies (e.g.,
<a href="https://bioregistry.io/doid">Human Disease Ontology</a>,
<a href="https://bioregistry.io/pato">Phenotype And Trait Ontology</a>). They have four
parts:</p>

<ol>
  <li>A URI prefix (in red; always the same)</li>
  <li>An ontology prefix (in orange)</li>
  <li>A delimiter (in black; always the same)</li>
  <li>An ontology local identifier (in blue)</li>
</ol>

<p><span style="color:red">http://purl.obolibrary.org/obo/</span><span style="color:orange">DRON</span>_<span style="color:blue">0000005</span></p>

<p>Confusingly, the entire combination of the ontology’s prefix, the delimiter, and
the ontology’s local identifier (e.g., <code class="language-plaintext highlighter-rouge">DRON_0000005</code>) are considered in some
contexts as a local identifier in a theoretical semantic space for OBO, whose
URI prefix is <code class="language-plaintext highlighter-rouge">http://purl.obolibrary.org/obo/</code>. This confusion lead to services
like Identifiers.org to denote these ontologies as having the “namespace
embedded in the local unique identifier” and therefore include the prefix again
in the regular expression pattern describing the local identifiers, e.g.
<code class="language-plaintext highlighter-rouge">^DOID:\d+$</code> for the Human Disease Ontology.</p>

<p>This notation of the regular expression makes no sense for several reasons:</p>

<ol>
  <li>The regular expression should correspond to the local identifiers of a
semantic space like <code class="language-plaintext highlighter-rouge">DOID</code>, not a registry like the OBO PURL system.</li>
  <li>If you follow the simple algorithm for constructing a CURIE from a prefix and
identifier, you end up with identifiers that look like CURIEs like
<code class="language-plaintext highlighter-rouge">DOID:11337</code> or redundant CURIEs that look like <code class="language-plaintext highlighter-rouge">DOID:DOID:11337</code>.</li>
  <li>Identifiers.org doesn’t even handle CURIEs constructed following the rules
for embedding the prefix in the local identifier.</li>
  <li>It creates ambiguities in spreadsheets where columns are supposed to contain
local identifiers or CURIEs.</li>
</ol>

<p>The solution is simply to drop the entire notion of namespaces embedded in local
unique identifiers. Since this would require updating a lot of data in a lot of
places, the interim solution is to programmatically normalize identifiers and
CURIEs in the meantime to remove instances of this redundancy.</p>

<h2 id="registry">Registry</h2>

<p>A registry is a special kind of semantic space that enumerates other semantic
spaces and assigns them local identifiers. Due to the connection with prefix
maps and CURIEs, the local identifiers in registries are also colloquially
called prefixes.</p>

<p>A registry also collects additional metadata about each semantic space,
including its name, its canonical prefix, its stylized prefix, additional prefix
synonyms, its homepage, an example local identifier, a regular expression
pattern for local identifiers, and one or more URI format strings from both
first-party and third-party sources. However, there are a wide variety of
metadata standards across various biomedical and semantic web registries, and
not all fields are included.</p>

<p>Like with semantic spaces, a high-quality registry should have an associated
first-party provider that comprises a website for exploring its entries and
their associated metadata.</p>

<!--
Some registries are directly imported and reused in other places (e.g., GO
Registry reused in
psi-mi-CV [https://github.com/HUPO-PSI/psi-ms-CV/blob/master/db-xrefs.yaml],
NCBI GenBank Registry reused in https://www.ddbj.nig.ac.jp/ddbj/db_xref-e.html).
-->

<h2 id="metaregistry">Metaregistry</h2>

<p>A metaregistry is a special kind of registry that assigns local identifiers to a
collection of registries; it could even contain an entry about itself. It
collects additional metadata about each registry, such as a description of its
metadata standards and capabilities. Most importantly, a metaregistry contains
mappings between equivalent entries in its constituent registries. Before the
publication of this article, to the best of our knowledge, there were no
dedicated metaregistries. Some registries such as
<a href="https://fairsharing.org/">FAIRSharing</a> and the
<a href="https://registry.identifiers.org/">MIRIAM/Identifiers.org</a> registry contain
limited numbers of entries referring to other registries (e.g.,
<a href="https://bioportal.bioontology.org">BioPortal</a>), but they neither delineate
these records as representing registries, provide additional metadata, nor
provide mappings.</p>

<p>The only metaregistry in the biomedical domain is the
<a href="https://bioregistry.io">Bioregistry</a>.</p>

<h2 id="resolver">Resolver</h2>

<p>A resolver uses a registry to generate a URI for a given CURIE based on the
registry’s default provider for the semantic space with the given prefix, then
redirects the requester to the constructed URI. Resolvers are different from
providers in that they are general for many semantic spaces and do not host
content themselves. Two well-known resolvers are
<a href="https://identifiers.org">Identifiers.org</a> and <a href="https://n2t.net">Name-To-Thing</a>.</p>

<p>Lookup Service A lookup service is like a provider but generalized to provide
for many semantic spaces. They typically have a URI format string into which a
compact identifier can be placed like <a href="http://www.ontobee.org/">OntoBee</a>, but
many require more complicated programmatic logic to construct. Some well-known
lookup services are the <a href="https://www.ebi.ac.uk/ols">OLS</a>,
<a href="http://aber-owl.net/">AberOWL</a>, OntoBee, and BioPortal.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="semantics" /><summary type="html"><![CDATA[There are a lot of terms that I’ve been throwing around when talking about the Bioregistry, so this blog post is a first draft of a gloassary of all of them.]]></summary></entry><entry><title type="html">How to Curate the INDRA Database</title><link href="https://cthoyt.com/2021/09/16/curating-indra.html" rel="alternate" type="text/html" title="How to Curate the INDRA Database" /><published>2021-09-16T12:30:00+00:00</published><updated>2021-09-16T12:30:00+00:00</updated><id>https://cthoyt.com/2021/09/16/curating-indra</id><content type="html" xml:base="https://cthoyt.com/2021/09/16/curating-indra.html"><![CDATA[<p>With the recent paper on Gilda and approaching INDRA 2 and INDRA database papers
coming up, I’ve put together a visual guide on how to curate statements
extracted by INDRA through the web interface at https://db.indra.bio.</p>

<p>Navigate to the INDRA database portal using <a href="https://db.indra.bio">this link</a>.</p>

<p><img src="/img/indra_curation/01_navigate_to_site.png" alt="The landing page of the INDRA Database search" /></p>

<p>Register for an account if you don’t already have one by clicking the register
box.</p>

<p><img src="/img/indra_curation/02_register.png" alt="Register for an account" /></p>

<p>Login with your email/password.</p>

<p><img src="/img/indra_curation/03_login.png" alt="Login" /></p>

<p>Enter search text. This can be a gene symbol, chemical, or any other biomedical
entity. In this tutorial, we’ll search for “AKT1.”</p>

<p><img src="/img/indra_curation/04_begin_search.png" alt="Begin search" /></p>

<p>Now, click the “Ground with Gilda” button.
<a href="https://github.com/indralab/gilda">Gilda</a> will automatically look up the most
likely database identifier that goes with your search string. It’s quite smart
and can even disambiguate different senses of the same word. If it’s not sure,
it will ask you to pick from a list.</p>

<p><img src="/img/indra_curation/05_input_text.png" alt="Input search text" /></p>

<p>In this example, I chose AKT1, which was pretty easy for Gilda to ground and
didn’t need me to check. It also reports the confidence in the grounding and the
namespace to which it grounded in the box. Click the search button to get going
to the next step!</p>

<p><img src="/img/indra_curation/06_ground_with_gilda.png" alt="Ground with Gilda" /></p>

<p>Now you will see the search results. On the left, it has many kinds of
statements that INDRA models. They’re hierarchical, meaning you can click on one
to expand to more specific statement types.</p>

<p>On the right are badges for the different sources that give evidence for each
statement. The ones with the black text correspond to databases, like BioGRID,
and the ones with white text correspond to reading systems, like REACH.</p>

<p><img src="/img/indra_curation/07_search_results.png" alt="View the search results" /></p>

<p>The next image shows the expansion of <code class="language-plaintext highlighter-rouge">AKT affects BAD</code> to
<code class="language-plaintext highlighter-rouge">AKT1 phosphorylates BAD</code>. Note that the third level is the same as the second -
this is because there are actually some more specific phosphorylation events
contained in here as well!</p>

<p>Open up the curation interface by pressing the pencil button next to the
evidence you want to curate. Note that some sources, like BioPAX (from Pathway
Commons) don’t give evidence text, so these aren’t appropriate for curation via
the INDRA Database.</p>

<p><img src="/img/indra_curation/08_navigate_search_results.png" alt="Navigate the hierarchical search results" /></p>

<p>Now that the curation menu has come up, you can select one of several error
types. Use your best judgement if a statement is really correct. Note that INDRA
does synonym disambiguation, so the label for the statement may not match to the
highlighted text. You can click the entity names in the statement header to open
pages with more information about the entities, including their synonyms.</p>

<p><img src="/img/indra_curation/09_open_curation_menu.png" alt="Open the curation menu" /></p>

<p>If you want to leave a note that explains why you made the curation you did,
that would be very helpful! Finally, smash that submit button.</p>

<p><img src="/img/indra_curation/10_select_curation_type.png" alt="Select the curation type" /></p>

<p>You’ve now contributed a curation! Thank you very much. You and the rest of the
scientific community will now disproportionately benefit from this small amount
of effort because of the large-scale extraction efforts of the INDRA Database
and combination with other curations.</p>

<p><img src="/img/indra_curation/11_submit_and_profit.png" alt="Submit the curation" /></p>

<hr />

<p>Stay tuned for the upcoming INDRA 2 paper that outlines how curations can be
used to assess the quality of each statement (at each level of hierarchical
abstraction, too) as well as the INDRA Database paper describing this resource.
We’re currently working on
<a href="https://github.com/indralab/indra_db/pull/187">updating the API</a> for bulk
downloading curations from the INDRA Database and for use with the INDRA
assemble corpus utility
<a href="https://github.com/sorgerlab/indra/blob/bc39dae6849b1fd484d83eabb3d2afee963a6298/indra/tools/assemble_corpus.py#L1669"><code class="language-plaintext highlighter-rouge">indra.tools.assemble_corpus.filter_by_curation()</code></a>.</p>

<p><strong>Update</strong> Here’s the INDRA 2
preprint:<a href="https://doi.org/10.1101/2022.08.30.505688">doi:10.1101/2022.08.30.505688</a></p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="nlp" /><category term="indra" /><category term="biocuration" /><summary type="html"><![CDATA[With the recent paper on Gilda and approaching INDRA 2 and INDRA database papers coming up, I’ve put together a visual guide on how to curate statements extracted by INDRA through the web interface at https://db.indra.bio.]]></summary></entry><entry><title type="html">What’s a CURIE, and Why You Should be Using Them</title><link href="https://cthoyt.com/2021/09/14/curies.html" rel="alternate" type="text/html" title="What’s a CURIE, and Why You Should be Using Them" /><published>2021-09-14T08:47:00+00:00</published><updated>2021-09-14T08:47:00+00:00</updated><id>https://cthoyt.com/2021/09/14/curies</id><content type="html" xml:base="https://cthoyt.com/2021/09/14/curies.html"><![CDATA[<p>Compact uniform resource identifiers, or CURIEs, are an important formalism for
referencing biomedical entities. This post explains what they are, how to write
them yourself, and a brief outline of how they fit in to the semantic web,
linked open data, and open biomedical ontology worlds.</p>

<p>In the semantic web, linked open data, and ontology communities, uniform
resource identifiers (URIs) are used to reference named entities. For a given
nomenclature, like the
<a href="https://www.ebi.ac.uk/chebi">Chemical Entities of Biological Interest (ChEBI)</a>,
URIs usually have two parts:</p>

<ol>
  <li>A URI prefix (in red)</li>
  <li>A unique local identifier from the given nomenclature (in orange)</li>
</ol>

<p>All the URIs from the same nomenclature will have the same URI prefix, but a
different unique local identifier. Here’s an example, using the ChEBI unique
local identifier for
<a href="https://www.ebi.ac.uk/chebi/searchId.do?chebiId=CHEBI:138488">alsterpaullone</a>:</p>

<p><span style="color:red">https://www.ebi.ac.uk/chebi/searchId.do?chebiId=</span><span style="color:orange">138488</span></p>

<h2 id="the-trouble-with-uris">The Trouble with URIs</h2>

<p>URIs are inconvenient because each named entity could be referenced by
potentially many URIs. For example, a URI could start with either <code class="language-plaintext highlighter-rouge">http</code> or
<code class="language-plaintext highlighter-rouge">https</code>. Even worse, there are several competing services that each try to mint
the <em>one true URI</em> for each. XKCD sums the ensuing chaos up pretty well:</p>

<p><a href="https://xkcd.com/927/">
<img style="max-width: 500px; text-align: center;" src="https://imgs.xkcd.com/comics/standards_2x.png" alt="A relevant XKCD comic on proliferating competing standards" />
</a></p>

<p>For the example molecule, alsterpaullone, here are some (but not all) of the
possible URIs that could be used to reference it:</p>

<table>
  <thead>
    <tr>
      <th>Provider</th>
      <th>URI</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>First-party</td>
      <td>https://www.ebi.ac.uk/chebi/searchId.do?chebiId=138488</td>
    </tr>
    <tr>
      <td>Identifiers.org</td>
      <td>https://identifiers.org/CHEBI:138488<br />https://identifiers.org/CHEBI/138488<br />http://identifiers.org/CHEBI:138488<br />http://identifiers.org/CHEBI/138488</td>
    </tr>
    <tr>
      <td>OBO Library PURL</td>
      <td>http://purl.obolibrary.org/obo/CHEBI_138488</td>
    </tr>
    <tr>
      <td>Name-to-Thing</td>
      <td>https://n2t.net/chebi:138488</td>
    </tr>
  </tbody>
</table>

<p>The real issue with URIs is that the URI prefix (the beginning part) doesn’t
really tell you anything. In fact, given a URI, you usually have to do some
detective work to figure out which nomenclature authority it goes with.</p>

<p>One solution was to use resolvers that create “persistent URLs”, but in the end,
there are many competing resolvers that don’t cover everything. For example, the
OBO PURL system doesn’t cover HGNC and UniProt. The Identifiers.org system
doesn’t cover many ontologies.</p>

<p>For practical purposes, it makes sense to keep track of the commonly used names
of each resource, then just generate the kinds of URIs that people might want
depending on what software or data systems they’re working with rather than
prescribing one URI to be the canonical one.</p>

<h2 id="come-to-the-dark-side-we-have-curies">Come to the Dark Side, We have CURIEs</h2>

<p>The solution is to use compact uniform resource identifiers (CURIEs), which
replace the URI prefix with a more approachable prefix. A CURIE has three parts:</p>

<ol>
  <li>A prefix (in red)</li>
  <li>A delimiter (in black)</li>
  <li>A unique local identifier from the given nomenclature (in orange)</li>
</ol>

<p>Since everyone agrees on what ChEBI is, it makes sense to use <code class="language-plaintext highlighter-rouge">chebi</code> as the
prefix for ChEBI unique local identifiers. Here’s the same example for
alsterpaullone, condensed as a CURIE:</p>

<p><span style="color:red">chebi</span><b>:</b><span style="color:orange">138488</span></p>

<p>The best part of a CURIE is that you can associate your favorite URI prefix with
its corresponding prefix depending on your use case. You can even have a
database that stores all of the possible ones for you. Replacing URIs with
prefixes is so common, that it’s a core part of the SPARQL query language, which
is used both in the semantic web and ontologies to traverse data stored in the
resource description framework (RDF) schema. Here’s an example SPARQL query that
has these prefixes prominently at the top:</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">prefix</span><span class="w"> </span><span class="nn">obo</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://purl.obolibrary.org/obo/&gt;</span><span class="w">
</span><span class="k">prefix</span><span class="w"> </span><span class="nn">owl</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/2002/07/owl#&gt;</span><span class="w">
</span><span class="k">prefix</span><span class="w"> </span><span class="nn">rdfs</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/2000/01/rdf-schema#&gt;</span><span class="w">
</span><span class="k">prefix</span><span class="w"> </span><span class="nn">rdf</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/1999/02/22-rdf-syntax-ns#&gt;</span><span class="w">

</span><span class="k">SELECT</span><span class="w"> </span><span class="nv">?x</span><span class="w"> </span><span class="nv">?p</span><span class="w"> </span><span class="nv">?y</span><span class="w">
</span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="p">{</span><span class="nv">?x</span><span class="w"> </span><span class="nn">rdfs</span><span class="o">:</span><span class="ss">subClassOf</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="k">a</span><span class="w"> </span><span class="nn">owl</span><span class="o">:</span><span class="ss">Restriction</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">owl</span><span class="o">:</span><span class="ss">onProperty</span><span class="w"> </span><span class="nv">?p</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">owl</span><span class="o">:</span><span class="ss">someValuesFrom</span><span class="w"> </span><span class="nv">?y</span><span class="w"> </span><span class="p">]</span><span class="w">
  </span><span class="p">}</span><span class="w">
  </span><span class="k">UNION</span><span class="w"> </span><span class="p">{</span><span class="w">
   </span><span class="nv">?x</span><span class="w"> </span><span class="nn">rdfs</span><span class="o">:</span><span class="ss">subClassOf</span><span class="w"> </span><span class="nv">?y</span><span class="w"> </span><span class="p">.</span><span class="w">
   </span><span class="k">BIND</span><span class="p">(</span><span class="nn">rdfs</span><span class="o">:</span><span class="ss">subClassOf</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="nv">?p</span><span class="p">)</span><span class="w">
  </span><span class="p">}</span><span class="w">
  </span><span class="nv">?x</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">owl</span><span class="o">:</span><span class="ss">Class</span><span class="w"> </span><span class="p">.</span><span class="w">
  </span><span class="nv">?y</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">owl</span><span class="o">:</span><span class="ss">Class</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>This example was borrowed from the SPARQL queries in the repository for the
<a href="https://github.com/OBOFoundry/COB/blob/master/src/sparql/edges.sparql">Core Ontology for Biology and Biomedicine (COB)</a>.
You don’t have to understand the SPARQL itself, just check the first 4 lines
that start with <code class="language-plaintext highlighter-rouge">prefix ...</code>.</p>

<h3 id="how-to-build-a-curie">How to Build a CURIE</h3>

<p>Most common vocabularies can be written as CURIEs. Here are a few examples:</p>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Prefix</th>
      <th>Example Unique Local Identifier</th>
      <th>Example CURIE</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Gene Ontology</td>
      <td>go</td>
      <td>0032571</td>
      <td>go:0032571</td>
    </tr>
    <tr>
      <td>HGNC</td>
      <td>hgnc</td>
      <td>16793</td>
      <td>hgnc:16793</td>
    </tr>
    <tr>
      <td>UniProt</td>
      <td>uniprot</td>
      <td>P0DP23</td>
      <td>uniprot:P0DP23</td>
    </tr>
    <tr>
      <td>Disease Ontology</td>
      <td>doid</td>
      <td>0110974</td>
      <td>doid:0110974</td>
    </tr>
    <tr>
      <td>Medical Subject Headings</td>
      <td>mesh</td>
      <td>C063233</td>
      <td>mesh:C063233</td>
    </tr>
  </tbody>
</table>

<p>As you might guess, most prefixes are either the acronym for a nomenclature
authority or the name itself. There are a few cases where this isn’t true, like
for Disease Ontology. Their acronym is DO, but then that add ID which is usually
shorthand for “identifier”, and therefore get <code class="language-plaintext highlighter-rouge">doid</code> as a prefix.</p>

<p>Just to reiterate, it’s really easy to make a CURIE. You take the prefix, a
colon, then the unique local identifier and smash them together! Note: some
communities, like the Open Biomedical Ontologies Foundry, like to stylize
prefixes with uppercase or mixed-case. If you live in URI world, this is a big
deal, but for most practical purposes, it’s nice to be able to just keep it all
lowercase.</p>

<p>How do you know what’s the right prefix for each resource? And who even keeps
track of this stuff? The <a href="https://bioregistry.io/">Bioregistry</a> keeps an
up-to-date list that you can browse or search
<a href="https://bioregistry.io/registry/">here</a>. My team at Harvard Medical School has
been building this with help from the community to serve the community needs
that previous registries didn’t - most importantly, to make the data open and
transparent and to enable community suggestions in an open and fair way.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="semantics" /><category term="CURIEs" /><summary type="html"><![CDATA[Compact uniform resource identifiers, or CURIEs, are an important formalism for referencing biomedical entities. This post explains what they are, how to write them yourself, and a brief outline of how they fit in to the semantic web, linked open data, and open biomedical ontology worlds.]]></summary></entry><entry><title type="html">How to Code with Me - Beyond Linters</title><link href="https://cthoyt.com/2021/09/13/beyond-linters.html" rel="alternate" type="text/html" title="How to Code with Me - Beyond Linters" /><published>2021-09-13T23:18:00+00:00</published><updated>2021-09-13T23:18:00+00:00</updated><id>https://cthoyt.com/2021/09/13/beyond-linters</id><content type="html" xml:base="https://cthoyt.com/2021/09/13/beyond-linters.html"><![CDATA[<p>This post is about my personal code style guide that are beyond the enforcement
of my <a href="/2020/04/25/how-to-code-with-me-flake8.html">flake8 plugins</a> or
<a href="https://github.com/psf/black"><code class="language-plaintext highlighter-rouge">black</code></a>. I’ll try and update it over time.</p>

<h2 id="exit-code-blocks-as-early-as-possible">Exit code blocks as early as possible</h2>

<p>Sometimes you have an <code class="language-plaintext highlighter-rouge">if</code> statement that does some stuff when the conditional
is true, or raises an error if it isn’t. Here’s the wrong way to write this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">cond</span><span class="p">:</span>
    <span class="p">...</span>  <span class="c1"># big code block
</span><span class="k">else</span><span class="p">:</span>
    <span class="k">raise</span> <span class="nb">ValueError</span>
</code></pre></div></div>

<p>It’s better to raise the value error as quickly as possible, because then you
can dedent the big code block. This is particularly good if you have lots of
nested code blocks in conditionals, loops, etc.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="ow">not</span> <span class="n">cond</span><span class="p">:</span>
    <span class="k">raise</span> <span class="nb">ValueError</span>

<span class="p">...</span>  <span class="c1"># big code block
</span></code></pre></div></div>

<p>The same thing is true for <code class="language-plaintext highlighter-rouge">return</code> statements. The following code where a value
is set then returned is bad:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">cond</span><span class="p">:</span>
    <span class="p">...</span> <span class="c1"># big code block
</span>    <span class="n">value</span> <span class="o">=</span> <span class="p">...</span> <span class="c1"># final line that assigns value
</span><span class="k">else</span><span class="p">:</span>
    <span class="n">value</span> <span class="o">=</span> <span class="p">...</span>  <span class="c1"># one-liner assigning value
</span><span class="k">return</span> <span class="n">value</span>
</code></pre></div></div>

<p>It’s better to flip the conditional and return the value quickly, since it only
takes one line to assign it in the <code class="language-plaintext highlighter-rouge">else block</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="ow">not</span> <span class="n">cond</span><span class="p">:</span>
    <span class="k">return</span> <span class="p">...</span>  <span class="c1"># one-liner assigning value
</span>
<span class="p">...</span> <span class="c1"># big code block
</span><span class="k">return</span> <span class="p">...</span> <span class="c1"># final line that assigns value
</span></code></pre></div></div>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="code with me" /><summary type="html"><![CDATA[This post is about my personal code style guide that are beyond the enforcement of my flake8 plugins or black. I’ll try and update it over time.]]></summary></entry><entry><title type="html">Pre-loading a PostgreSQL Docker Container</title><link href="https://cthoyt.com/2021/08/28/biolookup-docker.html" rel="alternate" type="text/html" title="Pre-loading a PostgreSQL Docker Container" /><published>2021-08-28T14:44:00+00:00</published><updated>2021-08-28T14:44:00+00:00</updated><id>https://cthoyt.com/2021/08/28/biolookup-docker</id><content type="html" xml:base="https://cthoyt.com/2021/08/28/biolookup-docker.html"><![CDATA[<p><a href="https://www.postgresql.org/">PostgreSQL</a> is a powerful relational database
management system that can be easily downloaded and installed from its
<a href="https://hub.docker.com/_/postgres">official image on DockerHub</a> using
<a href="https://www.docker.com/">Docker</a>. However, it’s not so straightforward to
preload your own data. This blog post is about preparing a derivative of the
base PostgreSQL Docker image that’s preloaded with your own database and pushing
it back to DockerHub for redistribution.</p>

<p><strong>Prerequisites</strong> I’m going to assume you have a modern version of docker
running. I’m using
<a href="https://hub.docker.com/editions/community/docker-ce-desktop-mac/">Docker Desktop for Mac</a>.
I’ve also installed PostgreSQL using <code class="language-plaintext highlighter-rouge">brew install postgresql</code>, which puts a
suite of command line utilities, including <code class="language-plaintext highlighter-rouge">createdb</code>, which I use in the middle
of this tutorial. I’m usually using <a href="https://fishshell.com/">fish</a>, but the
following instructions are given with Bourne-again shell (bash) syntax.</p>

<p><strong>Notes</strong> Throughout this post, I’ll shorten PostgreSQL to postgres. All the
commands in this tutorial are run from the shell of the host system, i.e. I did
not ssh or exec into the Docker image itself to run them.</p>

<h2 id="run-the-base-image">Run the base image</h2>

<p>The first step is to run a base image. This could be as simple as
<code class="language-plaintext highlighter-rouge">docker run postgres</code>, but there are a few options to add to make the rest of
this process more simple. I’ve written this post while preparing the docker
image for the Biolookup Service, so <code class="language-plaintext highlighter-rouge">biolookup</code> appears many times as
names/passwords throughout. I’ll definitely be writing another post soon about
what that service is and what it does (think
<a href="https://www.ebi.ac.uk/ols/index">Ontology Lookup Service</a>, but not just
restricted to ontologies).</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker pull postgres
<span class="nv">$ </span>docker run <span class="se">\</span>
    <span class="nt">-p</span> 5434:5432 <span class="se">\</span>
    <span class="nt">--name</span> postgres-biolookup <span class="se">\</span>
    <span class="nt">--detach</span> <span class="se">\</span>
    <span class="nt">-e</span> <span class="nv">POSTGRES_PASSWORD</span><span class="o">=</span>biolookup <span class="se">\</span>
    <span class="nt">-e</span> <span class="nv">PGDATA</span><span class="o">=</span>/var/lib/postgresql/pgdata <span class="se">\</span>
    <span class="nt">--shm-size</span> 1gb <span class="se">\</span>
    postgres
</code></pre></div></div>

<ol>
  <li><code class="language-plaintext highlighter-rouge">-p</code>/<code class="language-plaintext highlighter-rouge">--publish</code> This takes an argument looking like <code class="language-plaintext highlighter-rouge">&lt;X&gt;:&lt;Y&gt;</code>. The <code class="language-plaintext highlighter-rouge">&lt;Y&gt;</code>
corresponds to the port inside the docker container, and the <code class="language-plaintext highlighter-rouge">&lt;X&gt;</code>
corresponds to what’s visible outside. I’m mapping from the default postgres
port inside the container (i.e., 5432) to a non-default one outside
(i.e., 5434) to avoid conflict with my local installation of postgres.</li>
  <li><code class="language-plaintext highlighter-rouge">--name</code> This gives a nice name to the container for lookup later. This
doesn’t have to be the same as the name you give when you push to dockerhub,
but it’s probably better to stay consistent. If you don’t give one, docker
assigns a silly name for you.</li>
  <li><code class="language-plaintext highlighter-rouge">-d</code>/<code class="language-plaintext highlighter-rouge">--detach</code> Rather than running in my current shell, this backgrounds it.
Since I used <code class="language-plaintext highlighter-rouge">--name</code>, I can look up my image directly using
<code class="language-plaintext highlighter-rouge">$(docker ps --filter "name=postgres-biolookup" -q)</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">-e</code>/<code class="language-plaintext highlighter-rouge">--env</code> This allows you to specify environment variables.
    <ol>
      <li>Setting <code class="language-plaintext highlighter-rouge">POSTGRES_PASSWORD</code> explicitly sets the password for the default
postgres user (named <code class="language-plaintext highlighter-rouge">postgres</code>).</li>
      <li>Setting <code class="language-plaintext highlighter-rouge">PGDATA</code> ensures that a docker commit will actually persist the
database’s content. The cryptic path that came after is just the standard
path postgres uses. Move along.</li>
    </ol>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">--shm-size</code> By default, the shared memory is 64mb. When loading up this big
database, this caused the following crash:</p>

    <pre><code class="language-python-traceback">  ...
  File "/Users/cthoyt/dev/pyobo/src/pyobo/database/sql/cli.py", line 52, in load
    _load(
  File "/Users/cthoyt/dev/pyobo/src/pyobo/database/sql/loader.py", line 65, in load
    _load_definition(engine=engine, table=defs_table, path=defs_path, test=test)
  File "/Users/cthoyt/dev/pyobo/src/pyobo/database/sql/loader.py", line 97, in _load_definition
    _load_table(
  File "/Users/cthoyt/dev/pyobo/src/pyobo/database/sql/loader.py", line 311, in _load_table
    cursor.execute(sql)
psycopg2.errors.DiskFull: could not resize shared memory segment "/PostgreSQL.1699521131" to 67128576 bytes: No space left on device
</code></pre>

    <p>Luckily,
<a href="https://stackoverflow.com/questions/56751565/pq-could-not-resize-shared-memory-segment-no-space-left-on-device">StackOverflow</a>
had me covered and suggested increasing the shared memory to 1gb using
<code class="language-plaintext highlighter-rouge">--shm-size</code>.</p>
  </li>
</ol>

<h2 id="create-the-database">Create the database</h2>

<p>Creating the database on the already running postgres docker image is a bit more
straightforwards:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ PGPASSWORD</span><span class="o">=</span>biolookup createdb <span class="nt">-h</span> localhost <span class="nt">-p</span> 5434 <span class="nt">-U</span> postgres biolookup
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">PGPASSWORD=biolookup</code> sets the password in the environment when this command
gets run so there’s no need to manually interact with it. <code class="language-plaintext highlighter-rouge">-h</code> is for host, <code class="language-plaintext highlighter-rouge">-p</code>
is for password, and <code class="language-plaintext highlighter-rouge">-U</code> is for username. <code class="language-plaintext highlighter-rouge">-e</code> can be added optionally to show
the commands that are run for debugging. The final part <code class="language-plaintext highlighter-rouge">biolookup</code> is the name
of the database that gets created.</p>

<h2 id="load-the-database">Load the database</h2>

<p>The motivation for this post was to prepare a rather large database for the
Biolookup Service, which contains hundreds of millions of identifiers, names,
definitions, and alternative identifiers for biomedical entities. I’ve written
<a href="/2020/04/18/ooh-na-na.html">previously</a> about building this database,
which after an incredible effort boils down to running <code class="language-plaintext highlighter-rouge">pyobo database build</code>
from the shell. It also automatically uploads its contents to Zenodo:</p>

<table>
  <thead>
    <tr>
      <th>Data</th>
      <th>DOI</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Names</td>
      <td><a href="https://doi.org/10.5281/zenodo.4660694"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.4660694.svg" alt="DOI" /></a></td>
    </tr>
    <tr>
      <td>Definitions</td>
      <td><a href="https://doi.org/10.5281/zenodo.4662925"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.4662925.svg" alt="DOI" /></a></td>
    </tr>
    <tr>
      <td>Alternative Identifiers</td>
      <td><a href="https://doi.org/10.5281/zenodo.4661368"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.4661368.svg" alt="DOI" /></a></td>
    </tr>
    <tr>
      <td>Species</td>
      <td><a href="https://doi.org/10.5281/zenodo.5334738"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.5334738.svg" alt="DOI" /></a></td>
    </tr>
  </tbody>
</table>

<p>Loading the database requires the
<a href="https://github.com/biopragmatics/biolookup"><code class="language-plaintext highlighter-rouge">biolookup</code></a> Python package. It
automatically downloads the data from the latest releases on Zenodo if not
available locally, then puts it in the database.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>python <span class="nt">-m</span> pip <span class="nb">install </span>biolookup
<span class="nv">$ </span>biolookup load <span class="nt">--uri</span> postgresql+psycopg2://postgres:biolookup@localhost:5434/biolookup <span class="nt">--test</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">--test</code> makes the database only load 100K records instead of hundreds of
millions of records. For building a real database, remove this.</p>

<h2 id="commit-and-push-to-dockerhub">Commit and push to DockerHub</h2>

<p>The <code class="language-plaintext highlighter-rouge">docker commit</code> checks what the difference between the base image and the
current state of the image is. Because of the <code class="language-plaintext highlighter-rouge">-e PGDATA=...</code>, it also tracks
the new data added. For the Biolookup Service, the image has gone from about
300mb to almost 40gb, so be patient. I went and made breakfast while this was
happening and it was done by the time I came back. For reference, it was a
saturday morning American breakfast.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker commit <span class="se">\</span>
    <span class="nt">-a</span> <span class="s2">"Charles Tapley Hoyt &lt;cthoyt@gmail.com&gt;"</span> <span class="se">\</span>
    <span class="nt">-m</span> <span class="s2">"Added biolookup schema and data"</span> <span class="se">\</span>
    <span class="si">$(</span>docker ps <span class="nt">--filter</span> <span class="s2">"name=postgres-biolookup"</span> <span class="nt">-q</span><span class="si">)</span> <span class="se">\</span>
    biopragmatics/postgres-biolookup:latest
</code></pre></div></div>

<p>After committing, it’s time to push to DockerHub. You might need to do
<code class="language-plaintext highlighter-rouge">docker login</code> before this. The name of the image takes the form
<code class="language-plaintext highlighter-rouge">&lt;organization&gt;/&lt;name&gt;[:&lt;tag&gt;]</code>. Make sure you push to an organization that you
have rights to, and the tag (the part after the colon) is optional.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker push biopragmatics/postgres-biolookup:latest
</code></pre></div></div>

<p>You can check to see it was uploaded properly
<a href="https://hub.docker.com/repository/docker/biopragmatics/postgres-biolookup">here</a>.</p>

<h2 id="run-locally">Run locally</h2>

<p>Since the <code class="language-plaintext highlighter-rouge">biolookup</code> web application is automatically installed with PyOBO and
the database is now built locally, you can test it locally with:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>biolookup web <span class="nt">--sql</span> <span class="nt">--uri</span> postgresql+psycopg2://postgres:biolookup@localhost:5434/biolookup
</code></pre></div></div>

<h2 id="run-with-docker-compose">Run with Docker Compose</h2>

<p>You can use the following configuration as a <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> file to
orchestrate the pre-loaded database with the front-end web application (more
information on that <a href="https://github.com/biopragmatics/biolookup-docker/">here</a>):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3"</span>

<span class="na">services</span><span class="pi">:</span>
  <span class="na">app</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">biopragmatics/biolookup:latest</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">BIOLOOKUP_SQLALCHEMY_URI</span><span class="pi">:</span> <span class="s">postgresql+psycopg2://postgres:biolookup@database/biolookup</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">8765:8765"</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">database</span>
  <span class="na">database</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">biopragmatics/postgres-biolookup:latest</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">5432:5432"</span>
</code></pre></div></div>

<p>You can run this with <code class="language-plaintext highlighter-rouge">docker-compose up --detach</code>. When you do this, you need
to be patient (1-5 minutes) for the database to start up before making requests
from the web application. If you didn’t detach when running docker-compose,
postgres will actually log when it’s ready. If you try making a request before
it’s done starting, you’ll probably get an error message that looks like this:</p>

<pre><code class="language-python-traceback">Traceback (most recent call last):
  ...
  File "/usr/local/lib/python3.9/site-packages/sqlalchemy/pool/base.py", line 605, in __connect
    connection = pool._invoke_creator(self)
  File "/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/create.py", line 578, in connect
    return dialect.connect(*cargs, **cparams)
  File "/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/default.py", line 584, in connect
    return self.dbapi.connect(*cargs, **cparams)
  File "/usr/local/lib/python3.9/site-packages/psycopg2/__init__.py", line 122, in connect
    conn = _connect(dsn, connection_factory=connection_factory, **kwasync)
sqlalchemy.exc.OperationalError: (psycopg2.OperationalError) FATAL:  the database system is starting up
</code></pre>

<p>If you want to more correct about this, you can use the
<a href="https://www.postgresql.org/docs/current/app-pg-isready.html"><code class="language-plaintext highlighter-rouge">pg_isready</code></a> to
check the connection status of a PostgreSQL server. But I think it’s practical
enough just to wait a few minutes</p>

<h2 id="next-steps">Next Steps</h2>

<p>My next steps are to figure out the best way to automate the first three steps
(running the base image, creating the database, and loading the database) then
hopefully do it in an automated setting in GitHub Actions.</p>

<hr />

<p>Special thanks to <a href="https://github.com/bgyori">Ben Gyori</a> for outlining how to do
this and getting me going in the right direction. Thanks to
<a href="https://github.com/scolby33">Scott Colby</a> for feedback and pro docker tips.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="docker" /><category term="ontology" /><summary type="html"><![CDATA[PostgreSQL is a powerful relational database management system that can be easily downloaded and installed from its official image on DockerHub using Docker. However, it’s not so straightforward to preload your own data. This blog post is about preparing a derivative of the base PostgreSQL Docker image that’s preloaded with your own database and pushing it back to DockerHub for redistribution.]]></summary></entry><entry><title type="html">Machine Learning Needs More Generators</title><link href="https://cthoyt.com/2021/08/18/streaming.html" rel="alternate" type="text/html" title="Machine Learning Needs More Generators" /><published>2021-08-18T11:21:00+00:00</published><updated>2021-08-18T11:21:00+00:00</updated><id>https://cthoyt.com/2021/08/18/streaming</id><content type="html" xml:base="https://cthoyt.com/2021/08/18/streaming.html"><![CDATA[<p>I’ve spent the last two days cleaning up some research machine learning code
that blew up when I tried applying it to my own data due to memory constraints.
This post is about the antipattern that caused this, how I fixed it, and how you
can avoid it too.</p>

<p>The <a href="https://indralab.github.io/">INDRA Lab</a> has been collaborating with my old
PhD cellmate <a href="https://github.com/ddomingof">Daniel Domingo-Fernández</a> and his
master’s student <a href="https://github.com/helena-balabin/">Helena Balabin</a> on
cross-modal transformers. We’ve jointly pre-trained a transformer
(<a href="https://github.com/stonkgs/stonkgs">STonKGs</a>) on knowledge graph embeddings on
the <a href="https://db.indra.bio">INDRA database</a> and the associated evidence text for
each triple.</p>

<p>We then fine-tuned models for various downstream tasks, including one for rating
the correctness of a given statement (relevant since most are from large-scale
text mining). We’re very interested in comparing this to INDRA’s belief system
that will be described in an upcoming publication from our group.</p>

<p>I wanted to apply this fine-tuned model to the self-updating models in
<a href="https://emmaa.indra.bio/">EMMAA</a>, but ran into some memory errors since the
code was building up big lists, converting the lists into DataFrames using
<a href="https://emmaa.indra.bio/">pandas</a>, then exporting to disk as a TSV. The
solution is to use generator functions (that create iterable for use with for
loops) and write the results to a file directly. The basic anti-pattern looks
like this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>


<span class="k">def</span> <span class="nf">f</span><span class="p">(</span><span class="n">df</span><span class="p">:</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">:</span>
    <span class="n">new_rows</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">df</span><span class="p">.</span><span class="n">iterrows</span><span class="p">():</span>
        <span class="n">new_row</span> <span class="o">=</span> <span class="p">{</span>
            <span class="p">...</span>  <span class="c1"># somehow build up a new row, as a dictionary
</span>        <span class="p">}</span>
        <span class="n">new_rows</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">new_row</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">new_rows</span><span class="p">)</span>
</code></pre></div></div>

<p>There are two problems with this:</p>

<ol>
  <li>The input must be completely built up before calling <code class="language-plaintext highlighter-rouge">f()</code></li>
  <li>The output must be completely built up before returning</li>
</ol>

<p>This compounds for every function <code class="language-plaintext highlighter-rouge">f()</code>, <code class="language-plaintext highlighter-rouge">g()</code>, <code class="language-plaintext highlighter-rouge">h()</code>, and so on that takes in
the result from the last dataframe transformation, since they all have to keep
everything in memory. In <a href="https://github.com/stonkgs/stonkgs">STonKGs</a>, the
first transformation is to look up the embeddings for a given
source/target/evidence triple from both the knowledge graph embedding and the
pre-trained BERT language model. The second transformation is to pre-process the
embeddings. The third is to actually apply the STonKGs model to jointly embed
them. The fourth is to apply the fine-tuned model. Each of these happens at
varying speeds, but are unfortunately decoupled.</p>

<p>Python has really powerful tool for using ideas from
<a href="https://en.wikipedia.org/wiki/Functional_programming">functional programming</a>
in an approachable way that allows us to solve this issue. Rather than building
up a huge list, we can simply yield each piece so another function can consume
them with a for loop. When I refactor code that looks like this, I make a second
helper function that does the hard work, and try and maintain the original
function’s interface by calling the helper function:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>


<span class="k">def</span> <span class="nf">f</span><span class="p">(</span><span class="n">df</span><span class="p">:</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">f_helper</span><span class="p">(</span><span class="n">df</span><span class="p">))</span>


<span class="k">def</span> <span class="nf">f_helper</span><span class="p">(</span><span class="n">df</span><span class="p">:</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Iterable</span><span class="p">[</span><span class="n">Mapping</span><span class="p">[</span><span class="n">Any</span><span class="p">,</span> <span class="n">Any</span><span class="p">]]:</span>
    <span class="c1"># new_rows = []  # we don't need this anymore!
</span>    <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">df</span><span class="p">.</span><span class="n">iterrows</span><span class="p">():</span>
        <span class="n">new_row</span> <span class="o">=</span> <span class="p">{</span>
            <span class="p">...</span>  <span class="c1"># somehow build up a new row, as a dictionary
</span>        <span class="p">}</span>
        <span class="k">yield</span> <span class="n">new_row</span>
</code></pre></div></div>

<p>If you’re not familiar with <code class="language-plaintext highlighter-rouge">yield</code>, here are a two videos to get you thinking
aboout how to use loops like a Pythonista:</p>

<ul>
  <li><a href="https://www.youtube.com/watch?v=EnSu9hHGq5o&amp;list=PLPFmTfhIBiumfYT3rsa35fHJxabB78er1&amp;index=5">Loop like a native: while, for, iterators, generators (PyCon US 2013)</a></li>
  <li><a href="https://www.youtube.com/watch?v=ei71YpmfRX4&amp;list=PLPFmTfhIBiumfYT3rsa35fHJxabB78er1&amp;index=6">Trey Hunner - Comprehensible Comprehensions (PyCon 2020)</a></li>
</ul>

<p>Now that we have <code class="language-plaintext highlighter-rouge">f_helper</code>, we solved issue #2. The solution to issue #1 is to
have functions that take in only the parts that are needed to build up each new
row. This means you should accept an iterable, and have the high level function
slice up the dataframe or do whatever pre-procesing is necessary first:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>


<span class="k">def</span> <span class="nf">f</span><span class="p">(</span><span class="n">df</span><span class="p">:</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">:</span>
    <span class="n">it</span> <span class="o">=</span> <span class="p">(</span><span class="n">row</span> <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">df</span><span class="p">.</span><span class="n">iterrows</span><span class="p">())</span>
    <span class="k">return</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">f_helper</span><span class="p">(</span><span class="n">it</span><span class="p">))</span>


<span class="k">def</span> <span class="nf">f_helper</span><span class="p">(</span><span class="n">rows</span><span class="p">:</span> <span class="n">Iterable</span><span class="p">[</span><span class="n">Mapping</span><span class="p">[</span><span class="n">Any</span><span class="p">,</span> <span class="n">Any</span><span class="p">]])</span> <span class="o">-&gt;</span> <span class="n">Iterable</span><span class="p">[</span><span class="n">Mapping</span><span class="p">[</span><span class="n">Any</span><span class="p">,</span> <span class="n">Any</span><span class="p">]]:</span>
    <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">rows</span><span class="p">:</span>
        <span class="n">new_row</span> <span class="o">=</span> <span class="p">{</span>
            <span class="p">...</span>  <span class="c1"># somehow build up a new row, as a dictionary
</span>        <span class="p">}</span>
        <span class="k">yield</span> <span class="n">new_row</span>
</code></pre></div></div>

<p>With this refactoring, the code still does what it used to, but now you can
think about how you might string together <code class="language-plaintext highlighter-rouge">f_helper()</code>, <code class="language-plaintext highlighter-rouge">g_helper()</code>,
<code class="language-plaintext highlighter-rouge">h_helper()</code>, and so on directly, since they don’t need to get completely
materialized as a list. If you do the composition of several functions that take
in iterables and yield stuff (i.e., they return an iterable of the stuff that
gets yielded), then you don’t have to worry about running out of memory since it
only needs to have the results of one set of transformations from the
compositions of all functions at a time. In my case, I just printed the results
to a file and then it no longer needed to be in memory.</p>

<hr />

<p>This was a quick blog post I wrote while the code I updated was running. The
full PR I did can be found <a href="https://github.com/stonkgs/stonkgs/pull/7">here</a> for
reference. Feel free to chime in on that PR if you have some questions about how
I did this or get in touch with any of the contact info on the bottom of my
blog.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="machine learning" /><summary type="html"><![CDATA[I’ve spent the last two days cleaning up some research machine learning code that blew up when I tried applying it to my own data due to memory constraints. This post is about the antipattern that caused this, how I fixed it, and how you can avoid it too.]]></summary></entry><entry><title type="html">Organizing the Public Data about a Researcher</title><link href="https://cthoyt.com/2021/08/17/self-organization.html" rel="alternate" type="text/html" title="Organizing the Public Data about a Researcher" /><published>2021-08-17T11:21:00+00:00</published><updated>2021-08-17T11:21:00+00:00</updated><id>https://cthoyt.com/2021/08/17/self-organization</id><content type="html" xml:base="https://cthoyt.com/2021/08/17/self-organization.html"><![CDATA[<p>In a <a href="/2021/01/17/organization-organization.html">previous post</a>, I
described how to formalize the information about a research organization using
Wikidata. This post follows the same theme, but about this time about a given
researcher. Not only can you follow this post to make your own scientific
profile easier to find and navigate, but you can also use Wikidata to improve
the profiles of your co-workers and collaborators.</p>

<p>There are several <em>other</em> places besides Wikidata for putting your scholarly
profile. The first (and worst) is by making a resume in an awful Word document
or PDF. Much like what ends up in english prose in scientific texts, this is
where knowledge goes to die. The next less bad is to put it in a closed
repository like <a href="https://www.linkedin.com">LinkedIn</a>,
<a href="https://www.researchgate.net">ResearchGate</a>, <a href="https://www.scopus.com">Scopus</a>,
<a href="https://loop.frontiersin.org/">Loop</a>, or one of many others. These might be
nice, but don’t forget they’re companies and do not want people to be able to
use their troves without paying up. The runner-up is <a href="https://orcid.org">ORCID</a>,
which is absolutely amazing for providing persistent identifiers to individuals,
linking published work in high-profile repositories like PubMed, and in some
cases linking grants. While it will be the cornerstone of this tutorial, it
ultimately falls short as a tool for integrating the wide variety of information
available about any given researcher. So who’s the winner? Wikidata, of course!
It has an incredible flexibility for creating new relationships and a
first-class notion of scholarly works, awards, affiliations, employers, and the
relationships between them that make it a perfect place for storing information.</p>

<p>Anyone can edit Wikidata, meaning you can create an entry for your curmudgeonly
PI who wouldn’t be caught dead making a profile on this new thing called “the
internet”. There are three <em>notability</em> criteria for
<a href="https://www.wikidata.org/wiki/Wikidata:Notability">what’s allowed</a> to be added
as an item in Wikidata:</p>

<ol>
  <li>References another Wikimedia commmons page</li>
  <li>References a clearly defined entity/concept that has meaningful external
provenance</li>
  <li>Fulfills a structural need to make statements in other items more useful</li>
</ol>

<p>Adding researchers usually fits one or both of criteria 2 or 3. Further, there
are several people in the bibliometrics community who have created bots for
importing and aligning information from external sources that make the entries
that you supply so much more rich overnight (e.g., some bots automatically
translate the descriptions of pages into new languages, some import publication
metadata, some align identifiers with external resources).</p>

<p>Let’s get to it.</p>

<h2 id="step-1-orcid">Step 1: ORCID</h2>

<p>The ORCiD identifier is the single unambiguous identifier for each researcher.</p>

<blockquote><p lang="en" dir="ltr">As unique as you are, the truth is your name probably isn’t all that unique. ORCID was founded specifically to help solve the problem of name ambiguity in research. Want to learn more? Start here! <a href="https://twitter.com/hashtag/openscience">#openscience</a> <a href="https://twitter.com/hashtag/openresearch">#openresearch</a> <a href="https://twitter.com/hashtag/namedisambiguity">#namedisambiguity</a><a href="https://t.co/Q2J1bVrolc">https://t.co/Q2J1bVrolc</a></p>&mdash; ORCID Organization (@ORCID_Org) <a href="https://twitter.com/ORCID_Org/status/1427578459258249245?ref_src=twsrc%5Etfw">August 17, 2021</a></blockquote>

<p>If you don’t have one, it only takes 2 minutes to make one
<a href="https://orcid.org/register">here</a>. ORCID profiles look like a bunch of sets of
4 numbers/letters with dashes between them. Mine is
<a href="https://bioregistry.io/orcid:0000-0003-4423-4370">0000-0003-4423-4370</a>. You can
use a resolver like the Bioregistry to look up web pages on ORCID IDs with links
that look like
<a href="https://bioregistry.io/orcid:0000-0003-4423-4370">https://bioregistry.io/orcid:0000-0003-4423-4370</a>
or more specifically use ORCID’s web address directly like
<a href="https://orcid.org/0000-0003-4423-4370">https://orcid.org/0000-0003-4423-4370</a>.</p>

<p>It’s super intuitive how to put information in here. There are a few minimal
things to include to make it possible for people to find you:</p>

<ol>
  <li>Include any variations on your name that might appear in publications. In my
professional life, I write out my name in full, including my middle name, but
there are all sorts of variations that pop up including:
    <ul>
      <li>Charles Hoyt</li>
      <li>C.T. Hoyt</li>
      <li>C. Hoyt</li>
      <li>Hoyt, C.T.</li>
      <li>Hoyt, C.</li>
      <li>Hoyt, Charles Tapley</li>
    </ul>
  </li>
  <li>Include your education. Since most people reference their highest academic
degree in prose, this again helps people find you and disambiguate you from
other people. Even ORCID knows about this</li>
  <li>Make sure you set each field to “public” so everyone can see it! These are
marked as private by default, so make sure you click the following box
(thanks to Ann Reynolds for pointing this out)</li>
</ol>

<p><img src="/img/orcid_privacy.png" alt="ORCID Privacy box" /></p>

<p>I happen to enjoy spending time to maintain my
<a href="https://bioregistry.io/orcid:0000-0003-4423-4370">ORCID profile</a>, and here’s
what it looks like:</p>

<p><a href="https://bioregistry.io/orcid:0000-0003-4423-4370"><img src="/img/orcid_page.png" alt="ORCID Profile of Charles Tapley Hoyt" /></a></p>

<h2 id="step-2-wikidata">Step 2: Wikidata</h2>

<p>I’ll reiterate the short summary I gave on Wikidata from my previous post:</p>

<blockquote>
  <p>Wikidata is an open, community-curated platform of knowledge. It stores
entities, their relations to other entities, their relations to scalar values,
and added context for each relationship. Typically, relationships have a
subject, relation, and object and can be read like a simple sentence in the
english language.</p>

  <p>There are lots of working groups that maintain its ontology (i.e., the rules
for how curation should be done) around certain domains, such representing
organization structures. This means there are lots of tools already built in
to Wikidata for potential curators like you and me to create rich pages for
their organizations.</p>

  <p>One of the curation rules shared across all domains in Wikidata is that each
entity should have a “type”. This means on the page for
<a href="https://www.wikidata.org/wiki/Q937">Albert Einstein</a>, there is a relationship
stating he is an <a href="https://www.wikidata.org/wiki/Property:P31">instance of</a> a
<a href="https://www.wikidata.org/wiki/Q5">human</a>. The “instance of” item on Wikidata
is a special kind called a “property” and is one of the places where the
ontology lives - there are specific rules for each property on how it should
be used in a relationship, like what’s allowed to be the subject and what’s
allowed to be the object. For”instance of”, there are no rules about the
subject. However, the object of the relationship where “instance of” is the
property should be a “class” of thing. It wouldn’t make sense for the type of
another entity to be an instance of “Albert Einstein”.</p>
</blockquote>

<p>One thing to note: use the Wikidata search bar to see if there’s already a page
for the person you’re looking for! It’s a big problem if there are duplicate
entries on Wikidata. <a href="https://scholia.toolforge.org/">Scholia</a> also has some
tools for looking people up by ORCID or other identifiers that might be helpful.</p>

<h3 id="ontology-for-researchers-and-other-humans">Ontology for Researchers (and other Humans)</h3>

<p>Unlike an organizations, all researchers will have the type of
<a href="https://www.wikidata.org/wiki/Q5">human</a>. Typically, humans have certain pieces
of information associated with them using the following properties:</p>

<ul>
  <li><a href="https://www.wikidata.org/wiki/Property:P21">sex or gender</a> - for humans, this
can include: male, female, non-binary, intersex, transgender female,
transgender male, and agender.</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P27">country of citizenship</a></li>
  <li><a href="https://www.wikidata.org/wiki/Property:P103">native language</a> - there are
Wikidata entries for most (if not all) common languages</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P1412">languages spoken, written or signed</a> -
additional non-native languages. It doesn’t have any information about levels,
so I’m a bit hesitant about advertizing my B1 level of German!</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P1449">nickname</a> - a slightly more
specific place to put a nickname than the synonyms list at the top of the
page. For example, I go by Charles Tapley Hoyt professionally, but use Charlie
in my everyday life</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P856">official website</a> - one or more
personal websites for the researcher</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P1581">official blog</a> - one or more
blog written for the researcher. It happens that my blog and personal site are
on the same page.</li>
</ul>

<p>I’d highly suggest doing all of these before moving on to the next few sections,
which have quite a few bells and whistles.</p>

<p>Affiliations:</p>

<ul>
  <li><a href="https://www.wikidata.org/wiki/Property:P69">educated at (P69)</a> - links a
person to the institutions at which they earned their bachelor’s, master’s, or
doctoral degrees, as well as any other education.</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P108">employer (P108)</a> - the top-level
organization that you’re associated with. For me at the moment, that’s
<a href="https://www.wikidata.org/wiki/Q49121">Harvard Medical School</a>. For specific
departments or teams within the organization, you can use the “affiliation”
property.</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P1416">affiliation (P1416)</a> - useful
for subdivisions inside organizations. For example, while I am employed by
Harvard Medical School, I am appointed as a postdoc in the
<a href="https://www.wikidata.org/wiki/Q107380113">Laboratory of Systems Pharmacology</a>,
which itself has information about its membership within the
<a href="https://www.wikidata.org/wiki/Q107392274">Harvard Program in Therapeutic Science</a>.</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P463">member of (P463)</a> - any
professional societies, working groups, or other organizations. For example, I
belong to the
<a href="https://www.wikidata.org/wiki/Q247556">American Chemical Society</a>.</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P184">doctoral advisor (P184)</a> -
useful for building up academic trees. I asked my doctoral advisor about his
doctoral advisor and actually ended up emailing him to find out some more
information about my academic lineage. It turns out that there’s a lot of
information in the Academic Tree website that gets propagated through to
Wikidata already by bots. After I added a few missing links, I found out that
I am in the lineage of <a href="https://www.wikidata.org/wiki/Q76597">Emil Erlenmeyer</a>
after whom the eponymous Erlenmeyer flask is named. Depending on if you left
your doctoral work happy or not, you might not want to revisit this. Totally
understandable.</li>
</ul>

<p>Accomplishments:</p>

<ul>
  <li><a href="https://www.wikidata.org/wiki/Property:P512">academic degree (P512)</a> - the
highest academic degree obtained (e.g.,
<a href="https://www.wikidata.org/wiki/Q752297">Doctor of Philosophy</a>). Using the
“qualifiers”, it’s also possible to add information about the doctoral
advisor, when the degree was received, the opponents during the disputation
(i.e., the examination committee)</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P1026">academic thesis (P1026)</a> - make
an entry for your master’s or PhD thesis, and link them to you this way!</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P166">award received (P166)</a> - make an
entry for the awards you’ve received, then connect them here.</li>
</ul>

<p>Some of the affiliations and accomplishments will require going even deeper into
your curation and making additional Wikidata entries. For example, many awards
probably don’t already have a Wikidata entry. Same is likely true about your
academic thesis.</p>

<p>General External Account Links:</p>

<ul>
  <li><a href="https://www.wikidata.org/wiki/Property:P2037">GitHub username (P2037)</a></li>
  <li><a href="https://www.wikidata.org/wiki/Property:P6634">LinkedIn personal profile ID (P6634)</a></li>
  <li><a href="https://www.wikidata.org/wiki/Property:P2002">Twitter username (P2002)</a></li>
  <li><a href="https://www.wikidata.org/wiki/Property:P4033">Mastodon address (P4033)</a></li>
</ul>

<p>Academic External Account Links:</p>

<ul>
  <li><a href="https://www.wikidata.org/wiki/Property:P496">ORCID ID (P496)</a></li>
  <li><a href="https://www.wikidata.org/wiki/Property:P1960">Google scholar author ID (P1960)</a></li>
  <li><a href="https://www.wikidata.org/wiki/Property:P2456">DBLP author ID (P2456)</a></li>
</ul>

<p>The ORCID identifier is definitely the most important, but Google and DBLP often
pre-index researchers even if they have not got an ORCID, so this might be the
best you can do for some researchers.</p>

<p>There are tons of other IDs you can link as well as other properties. When I was
working on <a href="https://www.wikidata.org/wiki/Q47475003">my Wikidata entry</a>, I used
<a href="https://scholia.toolforge.org/author/Q20895241">Egon Willighagen’s</a> as an
example. I’d suggest browsing through ours to go to the next level.</p>

<h2 id="tutorial">Tutorial</h2>

<p>I’ll refer back to my <a href="/2021/01/17/organization-organization.html">previous
post</a> on curating
information about an organization, since it works basically the same way.</p>

<p>One of the important things I mentioned before is to add lots of synonyms for
your name, especially in ways they might appear in journal articles.</p>

<p><img src="/img/wikidata_researcher_synonyms.png" alt="Synonyms of Charles Tapley Hoyt" /></p>

<h2 id="scholia">Scholia</h2>

<p>If you do a really good job curating information about yourself, your
co-workers, and your affiliations, <a href="https://scholia.toolforge.org/">Scholia</a>
becomes a very powerful tool for showing information about you. For example,
here’s my Wikidata page rendered by Scholia:
<a href="https://scholia.toolforge.org/author/Q47475003">https://scholia.toolforge.org/author/Q47475003</a>.</p>

<p>One part that’s really cool is the co-author graph:</p>

<p><img src="/img/scholia_coauthor_graph.png" alt="Co-authors of Charles Tapley Hoyt" /></p>

<p>When you start off, there won’t be much here. Luckily, it has a curation link
that takes advantage of the synonyms on the Wikidata and helps connect papers
that are already on Wikidata to the given researcher’s page. Here’s the link for
mine (though I keep it pretty up-to-date, and it’s likely empty):
<a href="https://scholia.toolforge.org/author/Q47475003/curation">https://scholia.toolforge.org/author/Q47475003/curation</a></p>

<hr />

<p>There’s quite a bit more with respect to getting papers and associations between
researchers and events like conferences, but this is already a great place to
start. See the next post in the series on <a href="/2022/02/12/wikidata-publications.html">curating your papers in
Wikidata</a>.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="bibliometrics" /><summary type="html"><![CDATA[In a previous post, I described how to formalize the information about a research organization using Wikidata. This post follows the same theme, but about this time about a given researcher. Not only can you follow this post to make your own scientific profile easier to find and navigate, but you can also use Wikidata to improve the profiles of your co-workers and collaborators.]]></summary></entry><entry><title type="html">Reproducibly Loading the ChEMBL Relational Database</title><link href="https://cthoyt.com/2021/08/05/taming-chembl-sql.html" rel="alternate" type="text/html" title="Reproducibly Loading the ChEMBL Relational Database" /><published>2021-08-05T14:36:00+00:00</published><updated>2021-08-05T14:36:00+00:00</updated><id>https://cthoyt.com/2021/08/05/taming-chembl-sql</id><content type="html" xml:base="https://cthoyt.com/2021/08/05/taming-chembl-sql.html"><![CDATA[<p>In his blog post,
<a href="https://practicalcheminformatics.blogspot.com/2020/05/some-thoughts-on-comparing.html">Some Thoughts on Comparing Classification Models</a>,
Pat Walters illustrated enlightened ways to convey the results of training and
evaluating machine learning models on <a href="https://en.wikipedia.org/wiki/HERG">hERG</a>
activity data from ChEMBL (spoiler: it includes
<a href="https://en.wikipedia.org/wiki/Box_plot">box plots</a>). It started by querying the
ChEMBL relational database, but featured a common issue that hampers
reproducibility: hard-coded configuration to a local database based on a
specific database (MySQL). This blog post is about how to address this using
<a href="https://github.com/cthoyt/chembl-downloader"><code class="language-plaintext highlighter-rouge">chembl_downloader</code></a> and make code
using ChEMBL’s SQL dump more reusable and reproducible.</p>

<p>While the original blog post pointed to code
<a href="https://github.com/PatWalters/comparing_classifiers">PatWalters/comparing_classifiers</a>,
there’s an updated version at
<a href="https://github.com/PatWalters/jcamd_model_comparison">PatWalters/jcamd_model_comparison</a>
that includes the code that queries ChEMBL (among other things). The
<a href="https://nbviewer.jupyter.org/github/PatWalters/jcamd_model_comparison/blob/92cc912f24dcac5cad0c52143b67b8c2c124c11e/jcamd_model_comparison.ipynb">original notebook</a>
began like this in cells 2 and 4 (edited for clarity):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">mysql.connector</span> <span class="k">as</span> <span class="n">sql</span>
<span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>

<span class="n">sql</span> <span class="o">=</span> <span class="s">"""
select
    canonical_smiles,
    cs.molregno,
    md.chembl_id as mol_chembl_id,
    standard_relation, standard_value, standard_type, standard_units, description,
    td.organism, assay_type, confidence_score,
    td.pref_name, td.chembl_id as tgt_chembl_id
from activities act
    join assays ass on act.assay_id = ass.assay_id
    join target_dictionary td on td.tid = ass.tid
    join compound_structures cs on cs.molregno = act.molregno
    join molecule_dictionary md on md.molregno = cs.molregno
where ass.tid = 165
    and assay_type in ('B','F')
    and standard_value is not null
    and standard_units = 'nM'
    and act.standard_relation is not null
    and standard_type = 'IC50'
    and standard_relation = '='
"""</span>

<span class="k">with</span> <span class="n">sql</span><span class="p">.</span><span class="n">connect</span><span class="p">(</span>
    <span class="n">host</span><span class="o">=</span><span class="s">'localhost'</span><span class="p">,</span>
    <span class="n">database</span><span class="o">=</span><span class="s">'chembl_26'</span><span class="p">,</span>
    <span class="n">user</span><span class="o">=</span><span class="s">'pwalters'</span><span class="p">,</span>
    <span class="n">password</span><span class="o">=</span><span class="s">'itsasecret'</span><span class="p">,</span>
<span class="p">)</span> <span class="k">as</span> <span class="n">con</span><span class="p">:</span>
    <span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_sql</span><span class="p">(</span><span class="n">sql</span><span class="p">,</span> <span class="n">con</span><span class="o">=</span><span class="n">con</span><span class="p">)</span>
</code></pre></div></div>

<p>There are two main issues with this code:</p>

<ol>
  <li>It uses a specific database (MySQL) and user-specific connection
configuration which means:
    <ul>
      <li>Nobody else can run this code without editing it.</li>
      <li>There’s no information about how to get, preprocess, or load the database
before running the script.</li>
    </ul>
  </li>
  <li>It relies on a specific version of ChEMBL, which means that we can’t benefit
from new compounds and assays in new releases without editing it.</li>
</ol>

<p>To be fair, this is from a Jupyter notebook that’s not necessarily supposed to
be reused. But what if were so easy to fix this anti-pattern that there’s no
excuse not to? Here’s how using the
<a href="https://github.com/cthoyt/chembl-downloader"><code class="language-plaintext highlighter-rouge">chembl_downloader</code></a> Python
package:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">chembl_downloader</span>
<span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>

<span class="n">sql</span> <span class="o">=</span> <span class="p">...</span>  <span class="c1"># omitted for brevity
</span>
<span class="n">version</span> <span class="o">=</span> <span class="s">"26"</span>
<span class="k">with</span> <span class="n">chembl_downloader</span><span class="p">.</span><span class="n">connect</span><span class="p">(</span><span class="n">version</span><span class="o">=</span><span class="n">version</span><span class="p">)</span> <span class="k">as</span> <span class="n">con</span><span class="p">:</span>
    <span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_sql</span><span class="p">(</span><span class="n">sql</span><span class="p">,</span> <span class="n">con</span><span class="o">=</span><span class="n">con</span><span class="p">)</span>
</code></pre></div></div>

<p>With only a single (logical) line changed, this code now knows how to download
the ChEMBL 26 SQLite dump from the source, store it in a deterministic location,
extract it, and load with SQLite.</p>

<p>This means that anyone can run it without knowing how to download ChEMBL
themselves, which version to get, how to name the file, or where to put it on
their machine. It also relies on SQLite, which is effectively available on all
devices that run Python and has exactly the same programmatic API, but without
the need to run or connect to extra software. While a RDBMS like MySQL might be
more powerful for some kinds of queries, the difference is negligible when
querying single assays. It also implicitly solves the problem that the user
doesn’t know if there was any pre-processing done to the file.</p>

<p>Under the hood, it’s using the <a href="https://github.com/cthoyt/pystow"><code class="language-plaintext highlighter-rouge">pystow</code></a>
package to deterministically pick a folder (<code class="language-plaintext highlighter-rouge">~/.data/chembl/26/</code>) into which the
file
<code class="language-plaintext highlighter-rouge">ftp://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases/chembl_26/chembl_26_sqlite.tar.gz</code>
is download (<code class="language-plaintext highlighter-rouge">~/.data/chembl/26/chembl_26_sqlite.tar.gz</code>).</p>

<p>Since the pattern of connecting to the database then running a SQL query with
pandas is so common, the <code class="language-plaintext highlighter-rouge">chembl_downloader</code> has a <code class="language-plaintext highlighter-rouge">query()</code> function that wraps
the two lines from the previous example:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">chembl_downloader</span>

<span class="n">sql</span> <span class="o">=</span> <span class="p">...</span>  <span class="c1"># omitted for brevity
</span>
<span class="n">version</span> <span class="o">=</span> <span class="s">"26"</span>
<span class="n">df</span> <span class="o">=</span> <span class="n">chembl_downloader</span><span class="p">.</span><span class="n">query</span><span class="p">(</span><span class="n">sql</span><span class="p">,</span> <span class="n">version</span><span class="o">=</span><span class="n">version</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="getting-the-newest-version">Getting the Newest Version</h2>

<p>What about making this code automatically updating to the newest version of
ChEMBL? Just use the <code class="language-plaintext highlighter-rouge">chembl_downloader.latest()</code> to the latest version up for
you. Under the hood, it’s using the
<a href="https://github.com/biopragmatics/bioversions"><code class="language-plaintext highlighter-rouge">bioversions</code></a> package to do
this.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">chembl_downloader</span>

<span class="n">sql</span> <span class="o">=</span> <span class="p">...</span>  <span class="c1"># omitted for brevity
</span>
<span class="n">version</span> <span class="o">=</span> <span class="n">chembl_downloader</span><span class="p">.</span><span class="n">latest</span><span class="p">()</span>
<span class="n">df</span> <span class="o">=</span> <span class="n">chembl_downloader</span><span class="p">.</span><span class="n">query</span><span class="p">(</span><span class="n">sql</span><span class="p">,</span> <span class="n">version</span><span class="o">=</span><span class="n">version</span><span class="p">)</span>
</code></pre></div></div>

<p>Doing this is so common, that you can actually just omit the <code class="language-plaintext highlighter-rouge">version</code> argument
and it will look up the latest for you.</p>

<h2 id="meaningful-improvement">Meaningful Improvement</h2>

<p>I made a
<a href="https://github.com/PatWalters/jcamd_model_comparison/pull/1">pull request</a> to
update the notebook based on these suggestions. The new notebook is
<a href="https://nbviewer.jupyter.org/github/PatWalters/jcamd_model_comparison/blob/60f1ac2c62a6be957d78c6cf3a570946d714397a/jcamd_model_comparison.ipynb">here</a>
and features the most recent version of ChEMBL at the time of writing
(ChEMBL 29) instead of ChEMBL 26. The table below shows how much improvement
updating ChEMBL gives:</p>

<table>
  <thead>
    <tr>
      <th>Flag</th>
      <th style="text-align: right">ChEMBL 26</th>
      <th style="text-align: right">ChEMBL 29</th>
      <th style="text-align: right">Increase</th>
      <th style="text-align: right">Percent Increase</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Active</td>
      <td style="text-align: right">4191</td>
      <td style="text-align: right">4601</td>
      <td style="text-align: right">410</td>
      <td style="text-align: right">9%</td>
    </tr>
    <tr>
      <td>Inactive</td>
      <td style="text-align: right">2048</td>
      <td style="text-align: right">2274</td>
      <td style="text-align: right">226</td>
      <td style="text-align: right">11%</td>
    </tr>
  </tbody>
</table>

<p>I’d say getting 9% more actives and 11% more inactives basically for free by
writing better code is a pretty big success. In this notebook, the AUC-ROC of
the prominently presented LGBM classifier improved by about 1%. This could have
just as easily have gone down, but I think it was worth checking.</p>

<hr />

<p>One time, I received negative feedback from authors I asked why they hadn’t
updated the analysis they presented in their manuscript using the newest version
of ChEMBL (this was a few months ago, so the jump was from ChEMBL 25 to ChEMBL
28). One excuse they gave was that new data would (probably) not change their
results. Depending on what kind of stuff you do, 1% might be a big deal. Or not.</p>

<p>When I got that feedback, I checked in on the code that had been released along
with the manuscript to see if I could do it myself. It wasn’t pretty. I’d guess
the authors really just didn’t want to ever touch their code again because it
was very complicated, relied on tons of finnicky dependencies, and was overall
written poorly. I don’t think shaming scientists for writing bad code is a very
constructive nor a good way to motivate them to write better code. I’ve found on
many occassions that authors usually just don’t have the right training or
mindset to do reproducible/reusable science. A better solution is to offer pull
requests to their code that demonstrates how to fix the issues and explain in
detail how it works. Then, the best you can do is hope that they learn something
and use it in their next publication.</p>

<p>So for the case of these authors, I looked into their downstream dependencies
and began getting in touch with their code owners (maintainer would be a strong
word) then sending pull requests to make them more reusable. I also ended up
writing a bit of my own code to see if I could ultimately re-write the analysis
to be a little more automatic. This <code class="language-plaintext highlighter-rouge">chembl_downloader</code> package is one of the
tools I built along the way! I might come back and write a blog post about the
original paper that caused the negative feedback too, because it was indeed a
very cool paper! But first, I want to show that it can be reproduced.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="cheminformatics" /><category term="ChEMBL" /><category term="SQL" /><summary type="html"><![CDATA[In his blog post, Some Thoughts on Comparing Classification Models, Pat Walters illustrated enlightened ways to convey the results of training and evaluating machine learning models on hERG activity data from ChEMBL (spoiler: it includes box plots). It started by querying the ChEMBL relational database, but featured a common issue that hampers reproducibility: hard-coded configuration to a local database based on a specific database (MySQL). This blog post is about how to address this using chembl_downloader and make code using ChEMBL’s SQL dump more reusable and reproducible.]]></summary></entry><entry><title type="html">Reproducibly Loading the ChEMBL SDF</title><link href="https://cthoyt.com/2021/08/04/taming-chembl-sdf.html" rel="alternate" type="text/html" title="Reproducibly Loading the ChEMBL SDF" /><published>2021-08-04T12:46:00+00:00</published><updated>2021-08-04T12:46:00+00:00</updated><id>https://cthoyt.com/2021/08/04/taming-chembl-sdf</id><content type="html" xml:base="https://cthoyt.com/2021/08/04/taming-chembl-sdf.html"><![CDATA[<p><a href="https://www.ebi.ac.uk/chembl">ChEMBL</a> is easily the most useful database in a
cheminformatician’s toolbox, containing structural and activity information for
millions of diverse compounds. In his recent blog post,
<a href="https://greglandrum.github.io/rdkit-blog/tutorial/substructure/2021/08/03/generalized-substructure-search.html">Generalized Substructure Search</a>,
Greg Landrum highlighted some new RDKit features that enable more advanced
substructure queries. It started by loading molecules from the ChEMBL 29 SDF
dump, but it featured a common issue that hampers reproducibility: a hard-coded
local file path to the ChEMBL data. This blog post is how to address this using
<a href="https://github.com/cthoyt/chembl-downloader"><code class="language-plaintext highlighter-rouge">chembl_downloader</code></a> and make code
using ChEMBL’s SDF dump more reusable and reproducible.</p>

<h2 id="getting-data-reproducibly">Getting Data Reproducibly</h2>

<p>The code in the blog post began by loading up ChEMBL 29 like this (edited for
clarity and imports omitted for brevity):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">in_path</span> <span class="o">=</span> <span class="s">"/home/glandrum/Downloads/chembl_29.sdf.gz"</span>

<span class="k">with</span> <span class="n">gzip</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="n">in_path</span><span class="p">)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
    <span class="n">data</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">mol</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">rdkit</span><span class="p">.</span><span class="n">Chem</span><span class="p">.</span><span class="n">ForwardSDMolSupplier</span><span class="p">(</span><span class="nb">file</span><span class="p">)):</span>
        <span class="p">...</span>
        <span class="n">data</span><span class="p">.</span><span class="n">append</span><span class="p">(...)</span>

<span class="n">out_path</span> <span class="o">=</span> <span class="s">"../data/chembl29_sssdata.pkl"</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">out_path</span><span class="p">,</span> <span class="s">'wb'</span><span class="p">)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
    <span class="n">pickle</span><span class="p">.</span><span class="n">dump</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="nb">file</span><span class="p">)</span>
</code></pre></div></div>

<p>There are three main issues with this code:</p>

<ol>
  <li>It relies on a local file path to the ChEMBL data, which means:
    <ul>
      <li>Nobody else can run this code without editing it.</li>
      <li>There’s no information on how to get or preprocess this file before running
the script.</li>
    </ul>
  </li>
  <li>It relies on a specific version of ChEMBL, which means that we can’t benefit
from new compounds in new releases without editing it.</li>
  <li>It outputs data to a relative file path, which might not work based on the
way the script is run or the directory structure on your drive</li>
</ol>

<p>To be fair, this is a blog post that’s not necessarily supposed to be reused.
But what if were so easy to fix this anti-pattern that there’s no excuse not to?
Here’s how using the
<a href="https://github.com/cthoyt/chembl-downloader"><code class="language-plaintext highlighter-rouge">chembl_downloader</code></a> Python
package:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">chembl_downloader</span>

<span class="n">version</span> <span class="o">=</span> <span class="s">"29"</span>                                             <span class="c1"># &lt;-- This line changed for this example
</span><span class="n">in_path</span> <span class="o">=</span> <span class="n">chembl_downloader</span><span class="p">.</span><span class="n">download_sdf</span><span class="p">(</span><span class="n">version</span><span class="o">=</span><span class="n">version</span><span class="p">)</span>  <span class="c1"># &lt;-- This line changed for this example
</span>
<span class="k">with</span> <span class="n">gzip</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="n">in_path</span><span class="p">)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
    <span class="n">data</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">mol</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">rdkit</span><span class="p">.</span><span class="n">Chem</span><span class="p">.</span><span class="n">ForwardSDMolSupplier</span><span class="p">(</span><span class="nb">file</span><span class="p">)):</span>
        <span class="p">...</span>
        <span class="n">data</span><span class="p">.</span><span class="n">append</span><span class="p">(...)</span>

<span class="n">out_path</span> <span class="o">=</span> <span class="s">"../data/chembl29_sssdata.pkl"</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">out_path</span><span class="p">,</span> <span class="s">'wb'</span><span class="p">)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
    <span class="n">pickle</span><span class="p">.</span><span class="n">dump</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="nb">file</span><span class="p">)</span>
</code></pre></div></div>

<p>With only a single line changed, this code now knows how to download the ChEMBL
29 from the source and store it in a deterministic location on your hard drive.
This means that anyone can run it without knowing how to download ChEMBL
themselves, which version to get, how to name the file, or where to put it on
their machine. It also implicitly solves the problem that the user doesn’t know
if there was any pre-processing done to the file at
<code class="language-plaintext highlighter-rouge">"/home/glandrum/Downloads/chembl_29.sdf.gz"</code>. Under the hood, it’s using the
<a href="https://github.com/cthoyt/pystow"><code class="language-plaintext highlighter-rouge">pystow</code></a> package to determinisically pick a
folder (<code class="language-plaintext highlighter-rouge">~/.data/chembl/29/</code>) into which the file
<code class="language-plaintext highlighter-rouge">ftp://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases/chembl_29/chembl_29.sdf.gz</code>
is download (<code class="language-plaintext highlighter-rouge">~/.data/chembl/29/chembl_29.sdf.gz</code>).</p>

<h2 id="getting-the-newest-version">Getting the Newest Version</h2>

<p>What about making this code automatically updating to the newest version of
ChEMBL? Just use the <code class="language-plaintext highlighter-rouge">chembl_downloader.latest()</code> to the latest version up for
you. Under the hood, it’s using the
<a href="https://github.com/biopragmatics/bioversions"><code class="language-plaintext highlighter-rouge">bioversions</code></a> package to do
this.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">chembl_downloader</span>

<span class="n">version</span> <span class="o">=</span> <span class="n">chembl_downloader</span><span class="p">.</span><span class="n">latest</span><span class="p">()</span>                       <span class="c1"># &lt;-- This line changed for this example
</span><span class="n">in_path</span> <span class="o">=</span> <span class="n">chembl_downloader</span><span class="p">.</span><span class="n">download_sdf</span><span class="p">(</span><span class="n">version</span><span class="o">=</span><span class="n">version</span><span class="p">)</span>

<span class="k">with</span> <span class="n">gzip</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="n">in_path</span><span class="p">)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
    <span class="n">data</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">mol</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">rdkit</span><span class="p">.</span><span class="n">Chem</span><span class="p">.</span><span class="n">ForwardSDMolSupplier</span><span class="p">(</span><span class="nb">file</span><span class="p">)):</span>
        <span class="p">...</span>
        <span class="n">data</span><span class="p">.</span><span class="n">append</span><span class="p">(...)</span>

<span class="n">out_path</span> <span class="o">=</span> <span class="s">"../data/chembl29_sssdata.pkl"</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">out_path</span><span class="p">,</span> <span class="s">'wb'</span><span class="p">)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
    <span class="n">pickle</span><span class="p">.</span><span class="n">dump</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="nb">file</span><span class="p">)</span>
</code></pre></div></div>

<p>Note, if you omit the <code class="language-plaintext highlighter-rouge">version</code> argument completely, it automatically looks up
the version as well. However, there’s one more thing to update before we’ve
addressed our third point: where the file is output. There are two goals in
fixing the output:</p>

<ol>
  <li>Make the path deterministic</li>
  <li>Make the path based on the version of ChEMBL that’s being used, so if a newer
version gets used, it doesn’t delete the old file</li>
</ol>

<p>The solution comes by using <code class="language-plaintext highlighter-rouge">pystow</code> to pick a deterministic path, which the
<code class="language-plaintext highlighter-rouge">download_sdf()</code> function is actually using under the hood, too:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">chembl_downloader</span>

<span class="n">version</span> <span class="o">=</span> <span class="n">chembl_downloader</span><span class="p">.</span><span class="n">latest</span><span class="p">()</span>
<span class="n">in_path</span> <span class="o">=</span> <span class="n">chembl_downloader</span><span class="p">.</span><span class="n">download_sdf</span><span class="p">(</span><span class="n">version</span><span class="o">=</span><span class="n">version</span><span class="p">)</span>

<span class="k">with</span> <span class="n">gzip</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="n">in_path</span><span class="p">)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
    <span class="n">data</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">mol</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">rdkit</span><span class="p">.</span><span class="n">Chem</span><span class="p">.</span><span class="n">ForwardSDMolSupplier</span><span class="p">(</span><span class="nb">file</span><span class="p">)):</span>
        <span class="p">...</span>
        <span class="n">data</span><span class="p">.</span><span class="n">append</span><span class="p">(...)</span>

<span class="kn">import</span> <span class="nn">pystow</span>                                                  <span class="c1"># &lt;-- This line changed for this example
</span><span class="n">out_path</span> <span class="o">=</span> <span class="n">pystow</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="s">"chembl"</span><span class="p">,</span> <span class="n">version</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"sssdata.pkl"</span><span class="p">)</span>  <span class="c1"># &lt;-- This line changed for this example
</span><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">out_path</span><span class="p">,</span> <span class="s">'wb'</span><span class="p">)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
    <span class="n">pickle</span><span class="p">.</span><span class="n">dump</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="nb">file</span><span class="p">)</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">pystow.join</code> method creates a path to
<code class="language-plaintext highlighter-rouge">~/.data/chembl/&lt;version&gt;/sssdata.pkl</code>. Now this code is ready to stand the test
of time and a variety of different uses!</p>

<h2 id="coda">Coda</h2>

<p>Because the pattern of getting the SDF from ChEMBL then opening it with a
<code class="language-plaintext highlighter-rouge">ForwardSDMolSupplier</code> is so common, it’s actually included in its own function
<code class="language-plaintext highlighter-rouge">supplier()</code>. The code could be compressed one more time like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">chembl_downloader</span>

<span class="n">version</span> <span class="o">=</span> <span class="n">chembl_downloader</span><span class="p">.</span><span class="n">latest</span><span class="p">()</span>

<span class="k">with</span> <span class="n">chembl_downloader</span><span class="p">.</span><span class="n">supplier</span><span class="p">(</span><span class="n">version</span><span class="o">=</span><span class="n">version</span><span class="p">)</span> <span class="k">as</span> <span class="n">suppl</span><span class="p">:</span>
    <span class="n">data</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">mol</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">suppl</span><span class="p">):</span>
        <span class="p">...</span>
        <span class="n">data</span><span class="p">.</span><span class="n">append</span><span class="p">(...)</span>

<span class="kn">import</span> <span class="nn">pystow</span>

<span class="n">out_path</span> <span class="o">=</span> <span class="n">pystow</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="s">"chembl"</span><span class="p">,</span> <span class="n">version</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"sssdata.pkl"</span><span class="p">)</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">out_path</span><span class="p">,</span> <span class="s">'wb'</span><span class="p">)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
    <span class="n">pickle</span><span class="p">.</span><span class="n">dump</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="nb">file</span><span class="p">)</span>
</code></pre></div></div>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="cheminformatics" /><category term="ChEMBL" /><category term="SDF" /><summary type="html"><![CDATA[ChEMBL is easily the most useful database in a cheminformatician’s toolbox, containing structural and activity information for millions of diverse compounds. In his recent blog post, Generalized Substructure Search, Greg Landrum highlighted some new RDKit features that enable more advanced substructure queries. It started by loading molecules from the ChEMBL 29 SDF dump, but it featured a common issue that hampers reproducibility: a hard-coded local file path to the ChEMBL data. This blog post is how to address this using chembl_downloader and make code using ChEMBL’s SDF dump more reusable and reproducible.]]></summary></entry><entry><title type="html">Tales from the Bonner Ausländeramt</title><link href="https://cthoyt.com/2021/07/26/tales-from-the-auslaenderamt.html" rel="alternate" type="text/html" title="Tales from the Bonner Ausländeramt" /><published>2021-07-26T12:46:00+00:00</published><updated>2021-07-26T12:46:00+00:00</updated><id>https://cthoyt.com/2021/07/26/tales-from-the-auslaenderamt</id><content type="html" xml:base="https://cthoyt.com/2021/07/26/tales-from-the-auslaenderamt.html"><![CDATA[<p>This is a more personal blog post about my experience as an american expat in
Germany - specifically about my experiences at the Bonner Ausländeramt (Bonn’s
Foreigner’s Office of the City of Bonn).</p>

<p>I moved to Germany in August 2015 and immediately began my first incarnation of
the Ausländer Shuffle (which I affectionately name after the
<a href="https://huntnewsnu.com/49669/editorial/editorial-students-want-an-end-to-the-nu-shuffle/">NU Shuffle</a>).
Luckily, as a USA citizen, I entered the country with a 90-day tourist visa
which required no prior paperwork or pre-registration; just show up. In that 90
days, I had to get all of my paperwork in order, which included a local cell
phone number, proof of local housing, proof of German insurance, proof of
minimum bank account funds, and ultimately proof of enrollment at the
university. This was about as non-linear as a Kurt Vonnegut novel. I had the
benefit that my housing had been pre-registered by my study program. For
everyone else, the first thing they’d have to do is find housing, and even
worse, do it without the benefit of all the other paperwork that they couldn’t
get without already having housing.</p>

<p>After that was all done, I was able to sign up for my first appointment with the
Bonner Ausländeramt (Terminvereinbarung im Ausländeramt). Side bar: an
adjectival is when you turn a noun into an adjective. In the German language,
most places can be converted into an adjectival by adding the -er suffix. That’s
why it’s called Wiener Schnitzel - it’s Schnitzel from Wien (Vienna). Sign-ups
for appointments are online (https://www.bonn.de/antrag_auslaenderamt), then
they send you information about your appointment by snail mail. Unfortunately,
this office has not yet digitalized. You’re assigned a case worker based on your
last name, and you typically stay with them the entire time you live in the
city. They will keep a paper file in their office on you, next to all the other
paper files for all the other foreigners in Bonn. People from the USA are
typically given a two year visa when studying. In theory, this is more than
enough time for most study programs. Studying in Germany is cheap (just a
200-300 Euro administration per semester) so a lot of people decide to take
their time. I didn’t (oops).</p>

<p>My second encounter with the Ausländeramt was two years later after I had
finished my master’s degree in November 2017, before starting my Ph.D.. The
Ausländeramt snail mailed me unprompted a few months ahead of time with a date
and checklist of all the things I would need to renew my visa. They either have
a very boilerplate document for renewals ( most likely) or just assumed that my
studies hadn’t yet concluded, and therefore most of the document wasn’t
applicable. I had my Ph.D. work contract with Fraunhofer, but not yet proof of
my Ph.D. student status (I learned later that this is its own nightmare) but
they effectively let it slide and issued a new student/work visa. Side bar
again: it’s really great for companies to have students working for them. You
have so few rights when you’re also a student, and companies don’t have to
follow many of the worker laws. This is partially why Fraunhofer’s business
model to recruit as many students as possible to do most of the work is so great
for Fraunhofer.</p>

<p>My third encounter with the Ausländeramt was two years later after I had
finished my Ph.D. in December 2019, but they did not issue me an appointment
unprompted (I’m not sure why). I ended up finding the sign up form again, that I
had forgotten about from four years prior and signing up for an appointment a
few months in advance. After finishing graduate studies in Germany, you can get
an 18-month “work search visa” which is mostly the same as a work visa but is
issued even if you don’t already have a job. I was exited by this because I
wanted to take some time off. The only issue was they issued my appointment
after when my visa expired. This was particularly bad becuase it was in
December, and I wanted to travel for the holidays. If I did this and my visa
expired while I was out of the country, I would have a big problem - you can’t
re-enter for 6 months after your visa expires. I tried emailing (unsuccessfully)
and phoning the office every day for two whole weeks. They don’t answer the
phone. It turns out that sometimes it forwards to the City Hall, and they are
very annoyed to get questions about Ausländer affairs. With the end of December
approaching and still no plan how to get home for Christmas, I ended up going to
the office at 6:45AM (it opens at 8AM) one day try and get a walk-in
appointment. I was the 5th person in line, and by the time office opened at 8AM,
the line was going down the street. I knew I would be quick, but there were many
people who were unprepared, had extenuating circumstances, or were just a more
difficult case to deal with, so only a few of them would be seen between 8AM and
noon when the office closed (and it’s only open 4 days a week). Being 5th, I got
to see the walk-in administrator around 9:30AM, and they easily issued me a
Fiktionsbescheinigung after only a few minutes of describing my situation. That
document states that my case has basically been approved, but the paperwork is
still pending. Effectively, I could travel with my expired visa + this document
freely, and I then proceeded to book a very expensive last minute flight back
home. Unfortunately, my appointment was just after New Year’s, so I didn’t get
much vacation at home.</p>

<p>All the people I’ve talked to at the Ausländeramt were good people. They’re just
really understaffed, overworked, and without the benefit of competent
programmers and a progressive digitalization agenda to make their lives and the
lives of their clients better. Then the Coronavirus hit. Everything was shut
down for months. They issued emergency visa extensions since they couldn’t work
in office, and none of the work could be done from home. The facade collapsed
and it left many people confused, unsure, and anxious about their statuses.</p>

<p>My fourth encounter with the Ausländeramt should have been sometime around
today, since my visa is no longer valid tomorrow. Unfortunately, I signed up two
months ago for an appointment and never received any post. I emailed the office
to ask for a confirmation, and got nothing back. The office doesn’t answer their
phone line (this is the same now as it was before the pandemic). The difference
between this time and last time is when I went to get a walk-in appointment, I
was refused at the door by security because of pandemic precautions - only
appointment holders can enter. This is a bit of a Catch-22: Can’t go in the
office without an appointment. Can’t get an appointment without going in the
office.</p>

<p>It turns out they’re still rotating which staff are in on any given day and many
have home office (where they can’t really work, since all of it is done on paper
that lives in the office itself). I have been very anxious about this day coming
and officially tomorrow I will be in Germany illegally.</p>

<p>Last time I ended up on the phone line with the City Hall instead of the
Ausländeramt, they weren’t very sympathetic to my cause, and also realized that
they could effectively speak in their fastest and most ornamented German such
that I couldn’t understand them anymore. Luckily, my German friend offered to
help me this time with the shuffle and we got on the phone with the City Hall.
They were surprisingly helpful this time, but what happened next was a total
farce:</p>

<ol>
  <li>We talked to the nice lady at the city hall. She figured out what the office
number for my case worker was (this is very secret information) and shared it
with us so we could call directly.</li>
  <li>We called directly. It didn’t answer (just went on ringing). After trying
this at 15 minute intervals, we decided to call back the city and ask if we
could have another case worker’s number. She gave us the name and number of
another case worker in the office. We called them. They said that they would
share the number of the person who shared the office with my case worker
(these guys are cramped in there with all the files.)</li>
  <li>We called the office mate of my case worker. No answer (but this time, it cut
out after a few rings). We supposed that she was in office, but obviously
busy. We tried a few times, no luck.</li>
  <li>We tried the first case worker again that the nice lady at the city hall had
given us. She was busy too, so we had to wait a while before she picked up.
We asked her about the other lady, and she was nice enough to walk in the
other room and ask the second case worker when she would be available for us
to call.</li>
  <li>We called back when she was available. She looked up my file, saw my visa
expires tomorrow, was sympathetic to my cause, and said that she would snail
mail a Fiktionsbescheinigung to me. Luckily, I live in walking distance from
the Ausländeramt and this should arrive tomorrow.</li>
</ol>

<p>Let’s hope this means I won’t be here illegally come tomorrow! By the end of
this experience, I think we had spent way too much time of several people at the
Bonn City Hall and also the Ausländeramt. My friend had spent a few hours with
me, too, so what was the real cost of all of this? I actually still haven’t
solved my problem since I still don’t have an appointment to <em>actually</em> extend
my visa, either. My friend hadn’t experienced this kind of problem when he lived
in Belgium and France because EU citizens all share the right of free movement
between countries, and was just as jarring for him. He emailed our mayor,
<a href="https://www.bonn.de/vv/personen/Doerner-2379.php">Katja Dörner</a>, a nice letter
about the experience too. She’s part of the “Green Party” which is pretty
progressive, so maybe this will be a wake up call.</p>

<p>I’ve been lucky enough to be an easy case for the Ausländeramt - but it can’t be
overstated the advantage I have enjoyed based on my country of origin and my
educational status (among other things). However, most of the things I’ve done
at the Ausländeramt could have been automated. The system for creating
appointments should be 100% automated. It should be more granular as to what you
need from the appointment, and better inform clients what they need and better
allot time. The system should know when your visa expires, and automatically
issue a Fiktionsbescheinigung (perhaps with an apology note) if they can’t get
you an appointment before the expiration. Even most of the stuff that you need
to do at the appointment (present documents, etc.) can be automated through the
internet (even if only applicable to a subset of easy cases, like mine)! I’m
sure this would make this a much more pleasent experience for the case workers
and clients.</p>

<p>Update 2021-07-28: I’ve received a Fiktionsbescheinigung in the post, it was
signed and sent on the 26th. It’s good through the end of January 2022 - that’s
how behind the office is. Ooof.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="expat" /><summary type="html"><![CDATA[This is a more personal blog post about my experience as an american expat in Germany - specifically about my experiences at the Bonner Ausländeramt (Bonn’s Foreigner’s Office of the City of Bonn).]]></summary></entry><entry><title type="html">Pythagorean Mean Rank Metrics</title><link href="https://cthoyt.com/2021/04/19/pythagorean-mean-ranks.html" rel="alternate" type="text/html" title="Pythagorean Mean Rank Metrics" /><published>2021-04-19T12:46:00+00:00</published><updated>2021-04-19T12:46:00+00:00</updated><id>https://cthoyt.com/2021/04/19/pythagorean-mean-ranks</id><content type="html" xml:base="https://cthoyt.com/2021/04/19/pythagorean-mean-ranks.html"><![CDATA[<p>The mean rank (MR) and mean reciprocal rank (MRR) are among the most popular
metrics reported for the evaluation of knowledge graph embedding models in the
link prediction task. While they are reported on very different intervals
($\text{MR} \in [1,\infty)$ and $\text{MRR} \in (0,1]$, their deep
theoretical connection can be elegantly described through the lens of
<a href="https://en.wikipedia.org/wiki/Pythagorean_means">Pythagorean means</a>. This blog
post describes ideas <a href="https://github.com/mberr">Max Berrendorf</a> shared with me
that I recently implemented in <a href="https://github.com/pykeen/">PyKEEN</a> and later
wrote up as a <a href="https://arxiv.org/abs/2203.07544">full manuscript</a>.</p>

<p>If you want to cite some of the ideas in this post, you can use:</p>

<div class="language-bibtex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">@article</span><span class="p">{</span><span class="nl">hoyt2022metrics</span><span class="p">,</span>
    <span class="na">archivePrefix</span> <span class="p">=</span> <span class="s">{arXiv}</span><span class="p">,</span>
    <span class="na">arxivId</span> <span class="p">=</span> <span class="s">{2203.07544}</span><span class="p">,</span>
    <span class="na">author</span> <span class="p">=</span> <span class="s">{Hoyt, Charles Tapley and Berrendorf, Max and Gaklin, Mikhail and Tresp, Volker and Gyori, Benjamin M.}</span><span class="p">,</span>
    <span class="na">eprint</span> <span class="p">=</span> <span class="s">{2203.07544}</span><span class="p">,</span>
    <span class="na">month</span> <span class="p">=</span> <span class="s">{mar}</span><span class="p">,</span>
    <span class="na">title</span> <span class="p">=</span> <span class="s">{A Unified Framework for Rank-based Evaluation Metrics for Link Prediction in Knowledge Graphs}</span><span class="p">,</span>
    <span class="na">url</span> <span class="p">=</span> <span class="s">{https://arxiv.org/abs/2203.07544}</span><span class="p">,</span>
    <span class="na">year</span> <span class="p">=</span> <span class="s">{2022}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="the-formulation-of-the-link-prediction-task">The Formulation of the Link Prediction Task</h2>

<p>The link prediction task in knowledge graphs is effectively a binary
classification task for each potential triple $(h,
r, t)$ on whether it is true
or not. While the accuracy, precision, recall, $F_1$,
<a href="https://en.wikipedia.org/wiki/Matthews_correlation_coefficient">Matthews correlation coefficient</a>
(MCC), the area under
<a href="https://scikit-learn.org/stable/modules/generated/sklearn.metrics.precision_recall_curve.html">the precision-recall curve</a>,
and area under the
<a href="https://en.wikipedia.org/wiki/Receiver_operating_characteristic">receiver operating characteristic curve</a>
(AUC-ROC or AUROC) are typically used as metrics for binary classifications, the
link prediction task has the added twist that there are only positive examples
in a knowledge graph. This means that during training, potential negative
triples need to be sampled from the finite set of all possible triples. This is
called negative sampling within the knowledge graph embedding model literature
but more generally called <em>positive unlabeled learning</em> in the wider machine
learning community. This works because there’s a much higher likelihood that a
triple not already in the knowledge graph is negative than positive. Even if a
triple is sampled that should be positive, the model and training algorithm
should remain robust.</p>

<p>This also means that alternative metrics need to be introduced that don’t rely
on the existence or non-existence of negative examples - enter hits@k , mean
rank, and mean reciprocal rank. Each of these relies on the concept of a ranking
(not to be confused with the rank of a matrix) that is calculated based on
combining the true and sampled triples into a list, then sorting based on their
scores (calculated by the trained knowledge graph embedding model). The ranks
are the positions of the positive triples in this list, where lower ranks
correspond to better performance. For the molecular biologists reading, this is
very similar to the rank-based visual inspection of
<a href="https://www.gsea-msigdb.org/gsea">GSEA</a> results on differential gene expression
experiments. The hits@k, mean rank, and mean reciprocal rank are all summary
statistics over the set of ranks (again, the positions in the list of all true
triples + sampled triples, sorted by score).</p>

<h2 id="definitions-of-rank-based-metrics">Definitions of Rank-based Metrics</h2>

<p>In the following definitions, I’m going to use $\mathcal{I}$ to denote the set
of all ranks for true triples.</p>

<h3 id="hits--k">Hits @ K</h3>

<p>While this post isn’t about hits@k, it’s worth summarizing the alternative
approach that it takes to summarizing the rank list because it’s a much more
application-driven metric. Effectively, the hits@k describes the fraction of
true entities that appear in the first $k$ entities of the sorted rank list. It
is given as:</p>

\[\text{score}_k = \frac{1}{|\mathcal{I}|} \sum \limits_{r \in \mathcal{I}} \mathbb{I}[r \leq k]\]

<p>For example, if Google shows 20 results on the first page, then the percentage
of results that are relevant is the hits @ 20. The hits@k, regardless of $k$,
lies between $(0, 1]$ where closer to 1 is better.</p>

<p>This metric does not differentiate between cases when the rank is larger than
$k$. This means that a miss with rank $k+1$ and $k+d$ where $d \gg 1$ have the
same effect on the final score. Therefore, it is less suitable for the
comparison of different models.</p>

<h3 id="mean-rank">Mean Rank</h3>

<p>The mean rank (MR) computes the arithmetic mean over all individual ranks. It is
given as:</p>

\[\text{MR} =\frac{1}{|\mathcal{I}|} \sum \limits_{r \in \mathcal{I}} r\]

<p>It has the advantage over hits@k that it is sensitive to any model performance
changes, not only what occurs under a certain cutoff and therefore reflects
average performance. With PyKEEN’s standard 1-based indexing, the mean rank lies
on the interval $\text{MR} \in [1,\infty)$ where lower is better.</p>

<p>While it remains interpretable, the mean rank is dependent on the number of
candidates. A mean rank of 10 might indicate strong performance for a large
candidate set size of 1,000,000, but incredibly poor performance for a small
candidate set size of 20.</p>

<h3 id="mean-reciprocal-rank">Mean Reciprocal Rank</h3>

<p>The mean reciprocal rank (MRR) is the arithmetic mean of reciprocal ranks, which
can alternatively be formulated as the inverse of the harmonic mean of ranks. It
is defined as:</p>

\[\text{MRR} =\frac{1}{|\mathcal{I}|} \sum_{r \in \mathcal{I}} r^{-1} = \bigg(\frac{|\mathcal{I}|}{ \sum_{r \in
\mathcal{I}} r^{-1}}\bigg)^{-1}\]

<p>It has been argued that the mean reciprocal rank has theoretical flaws by
<a href="https://pykeen.readthedocs.io/en/latest/references.html#fuhr2018">Fuhr (2018)</a>.
However, this opinion is not undisputed, cf.
<a href="https://pykeen.readthedocs.io/en/latest/references.html#sakai2021">Sakai (2021)</a>.
Despite its <em>potential</em> flaws, MRR is still often used during early stopping due
to its behavior related to low rank values. While the hits@k ignores changes
among high rank values completely, and the mean rank changes uniformly across
the full value range, the mean reciprocal rank is more affected by changes of
low rank values than high ones without disregarding them completely like hits@k
does for low rank values. Therefore, it can be considered as soft a version of
hits@k that is less sensitive to outliers. It is bound on
$\text{MRR} \in (0, 1]$ where closer to 1 is better.</p>

<h3 id="inverse-arithmetic-mean-rank-and-harmonic-mean-rank">Inverse Arithmetic Mean Rank and Harmonic Mean Rank</h3>

<p>It’s odd that the mean rank and mean reciprocal rank are formulated in a way
such that $\text{MR} \in [1,\infty)$ and $\text{MRR} \in (0,1]$. Since the
mean reciprocal rank is just the inverse of the harmonic mean rank, then it
would make sense to report the harmonic mean rank (HMR) as well, defined by:</p>

\[\text{HMR} = \frac{|\mathcal{I}|}{ \sum_{r \in \mathcal{I}} r^{-1}} = \frac{1}{\text{MRR}}\]

<p>It has the benefit that it’s more easily comparable to the mean rank because
$\text{HMR} \in [1,\infty)$. Alternatively, the inverse arithmetic mean rank
(IAMR) could be defined as the inverse of the mean rank (which, remember, is
really the arithmetic mean rank) by:</p>

\[\text{IAMR} = \bigg(\frac{1}{|\mathcal{I}|} \sum \limits_{r \in \mathcal{I}} r \bigg)^{-1} = \frac{1}{\text{MR}}\]

<p>This has the benefit that it’s more comparable to the mean reciprocal rank
because $\text{IAMR} \in (0,1]$.</p>

<h2 id="demistying-the-metrics">Demistying the Metrics</h2>

<p>Since we’re thinking about arithmetic means and harmonic means over the ranks,
it would make sense to investigate the third
<a href="https://en.wikipedia.org/wiki/Pythagorean_means">Pythagorean mean</a>: the
geometric mean.</p>

<p><img style="max-width: 500px; text-align: center" src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/MathematicalMeans.svg/2560px-MathematicalMeans.svg.png" alt="Pythagorean Mean Diagram" /></p>

<p>This figure (from Wikimedia) shows the relationship between the arithmetic mean
(A), geometric mean (G), and harmonic mean (H) on a triangle circumscribed by a
circle of diameter $a+b$.</p>

<h3 id="getting-geometrified">Getting Geometrified</h3>

<p>For sets of positive numbers (which ranks always are), there is a nice property
that the arithmetic mean is the biggest, geometric mean is next, and the
harmonic mean is the smallest. Since the mean rank (based on the arithmetic
mean) tends to be biased towards low ranks, and the mean reciprocal rank (based
on the harmonic mean) tends to be biased towards high ranks, it could be the
case that geometric mean of ranks might balance these two biases. We can define
the geometric mean rank (GMR) as:</p>

\[\text{GMR} = \sqrt[\|\mathcal{I}\|]{\prod \limits_{r \in \mathcal{I}} r}\]

<p>Like the MR and HMR, $\text{GMR} \in [1,\infty)$. Like with the other
metrics, its inverse, the inverse geometric mean rank (IGMR) can be defined as:</p>

\[\text{IGMR} = \frac{1}{\text{GMR}} = \bigg(\sqrt[\|\mathcal{I}\|]{\prod \limits_{r \in \mathcal{I}} r}\bigg)^{-1}\]

<p>where $\text{IGMR} \in (0,1]$.</p>

<h3 id="additional-statistics">Additional Statistics</h3>

<p>While we proposed the geometric mean rank and its inverse after examining the
relationships between the mean rank and mean reciprocal rank, the idea of
reporting aggregations over the set of ranks could be extended much further. For
example, it makes sense to report other aggregate statistics over the ranks,
such as the median rank. This could prove to be more robust to outliers, but it
is not clear whether outlier ranks are a major issue in the utility of the
existing evaluation metrics or for the newly proposed one. Reporting the
standard deviation of ranks, the variance of ranks, and the median absolute
deviation of ranks could even enable statistical testing between the results
from two different models trained and evaluated on the same dataset, which has
been sorely lacking in previous benchmarking studies on using knowledge graph
embedding models for link prediction.</p>

<h2 id="usage-in-pykeen">Usage in PyKEEN</h2>

<p>PyKEEN has always been focused on easy usage. If you want to get these metrics,
do the following:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pykeen.pipeline</span> <span class="kn">import</span> <span class="n">pipeline</span>

<span class="n">results</span> <span class="o">=</span> <span class="n">pipeline</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s">'PairRE'</span><span class="p">,</span> <span class="n">dataset</span><span class="o">=</span><span class="s">'FB15k-237'</span><span class="p">,</span> <span class="p">...)</span>
<span class="n">igmr</span> <span class="o">=</span> <span class="n">results</span><span class="p">.</span><span class="n">get_metric</span><span class="p">(</span><span class="s">'inverse_geometric_mean_rank'</span><span class="p">)</span>
</code></pre></div></div>

<p>The rules for <code class="language-plaintext highlighter-rouge">get_metric()</code> are outlined
<a href="https://pykeen.readthedocs.io/en/latest/api/pykeen.evaluation.RankBasedMetricResults.html#pykeen.evaluation.RankBasedMetricResults.get_metric">here</a>.
Usually, you should write out the metric name in full, with underscores in the
middle. The common ones like <code class="language-plaintext highlighter-rouge">mrr</code>, <code class="language-plaintext highlighter-rouge">mr</code>, and <code class="language-plaintext highlighter-rouge">igmr</code> can be accessed by synonym.</p>

<hr />

<p>We’ve implemented all of these statistics in PyKEEN
<a href="https://github.com/pykeen/pykeen/pull/381">pull request #381</a>, reported for the
left sided, right sided, and two-sided evaluation as well as for the optimistic,
pessimistic, and realistic rankings. This blog post was adapted and extended
from the
<a href="https://pykeen.readthedocs.io/en/stable/tutorial/understanding_evaluation.html">PyKEEN documentation</a>.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="pykeen" /><summary type="html"><![CDATA[The mean rank (MR) and mean reciprocal rank (MRR) are among the most popular metrics reported for the evaluation of knowledge graph embedding models in the link prediction task. While they are reported on very different intervals ($\text{MR} \in [1,\infty)$ and $\text{MRR} \in (0,1]$, their deep theoretical connection can be elegantly described through the lens of Pythagorean means. This blog post describes ideas Max Berrendorf shared with me that I recently implemented in PyKEEN and later wrote up as a full manuscript.]]></summary></entry><entry><title type="html">Current Perspectives on KGEMs in and out of Biomedicine</title><link href="https://cthoyt.com/2021/04/05/kgem-perspectives.html" rel="alternate" type="text/html" title="Current Perspectives on KGEMs in and out of Biomedicine" /><published>2021-04-05T12:13:00+00:00</published><updated>2021-04-05T12:13:00+00:00</updated><id>https://cthoyt.com/2021/04/05/kgem-perspectives</id><content type="html" xml:base="https://cthoyt.com/2021/04/05/kgem-perspectives.html"><![CDATA[<p>After many discussions scientists from AstraZeneca’s knowledge graph and target
prioritization platform (BIKG) about the
<a href="https://github.com/pykeen/pykeen">PyKEEN</a> knowledge graph embedding model
package, I joined them in writing a
<a href="https://arxiv.org/abs/2102.10062">review on biomedical knowledge graphs</a>. I’m
giving a talk in their group tomorrow - this blog post is a longer form of some
ideas I’ll be presenting there. Here are
<a href="https://docs.google.com/presentation/d/1JyTkf58LnK7OEiwYUtqkHXuk4n3le3-mAWSI6t4IEp8">the slides</a>.</p>

<h3 id="who-is-the-arbiter-of-benchmarks">Who is the Arbiter of Benchmarks?</h3>

<p>The publication of each new knowledge graph embedding model is usually
accompanied by an evaluation of the link prediction task on common benchmark
datasets and a comparison to previous models’ performances on those datasets.
There’s a variety of ways authors report this, either by copying the original
results from the original papers describing the competitor models, by re-running
the author’s original code, by reimplementing the code themselves, or by running
an implementation from a larger knowledge graph embedding model package.</p>

<p>Who can be trusted? The original authors of each competitor model were/are
motivated to show that theirs was better than others, so their paper might have
inflated metrics. The authors of the current paper are motivated to show that
previous models are worse, so they may deflate metrics for old models (and
inflate theirs). Knowledge graph embedding model packages are motivated to
inflate metrics to show that using their package is better – I’ve indeed been
asked questions about why other packages are “performing better” than PyKEEN and
had to answer that they’re just evaluating differently that is less
conservative, and sometimes misleading. Even in a world without these competing
interests, it’s very hard to understand each other’s code, especially since it’s
not just about the model itself but also the training and evaluation pipeline.</p>

<p>This motivates large benchmarking studies that use unified frameworks and
re-implementations of models to investigate the relative performances of models
(and can for a brief moment, let you throw away your metric fixation). The
<a href="https://arxiv.org/abs/2006.13365">PyKEEN benchmark</a> is the largest and most
comprehensive to date, covering many models, datasets, and configurations. I was
proudly a part of this work under the leadership of Mehdi Ali
<a href="https://github.com/mali-git/">@mali-git</a> and with an amazing
<a href="https://pykeen.github.io/organization/">team</a>. There’s also the smaller
<a href="https://openreview.net/forum?id=BkxSmlBFvr">LibKGE benchmark</a> as well as
several others that we’re maintaining in a curated list in this
<a href="https://pykeen.github.io/kgem-meta-review/">meta-review</a> repository.</p>

<p>The <a href="https://ogb.stanford.edu/">Open Graph Benchmark (OGB)</a> provides a potential
solution to these issues by externalizing and standardizing the code that
evaluates the results. There are still sneaky ways to game this that I won’t
describe in detail, but I like the concept. However, the framework does not
cover some of the more popular datasets and the ones that it does introduce in
the link prediction task, it does not do so with any provenance on how they’re
created. During my talk, Benedek Rozemberczki
(<a href="https://github.com/benedekrozemberczki">@benedekrozemberczki</a>) brought up
another interesting point - many of the benchmarks in OGB have surpassed the
size that can be addressed by the physical infrastructure or cloud compute
budgets of many small academic institutions. As machine learning continues to
rely on larger, more exotic models, we will soon have to reckon with this
divide.</p>

<h3 id="testingvalidation-leakage">Testing/Validation Leakage</h3>

<p>Leakage is when triples in the testing/validation sets can be trivially inferred
from triples in the training set. This leads to an over-estimation in the
performance of the model.</p>

<p>The most common form of leakage occurs when a knowledge graph contains inverse
triples. An example:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">A</code> and <code class="language-plaintext highlighter-rouge">B</code> are entities in a knowledge graph</li>
  <li><code class="language-plaintext highlighter-rouge">part of</code> and <code class="language-plaintext highlighter-rouge">has part</code> are inverse relations in the knowledge graph</li>
  <li><code class="language-plaintext highlighter-rouge">A part of B</code> is in the training set</li>
  <li><code class="language-plaintext highlighter-rouge">B has part A</code> is in the testing set</li>
</ul>

<p>This scenario occurs in two datasets widely used in knowledge graph embedding
model benchmarking: the FreeBase 15K (FB15k) dataset proposed by
<a href="http://papers.nips.cc/paper/5071-translating-embeddings-for-modeling-multi-relational-data.pdf">Bordes <em>et al.</em> (2013)</a>
alongside the
<a href="https://pykeen.readthedocs.io/en/latest/api/pykeen.models.TransE.html">TransE model</a>
and the WordNet-18 ( WN18) dataset proposed by
<a href="https://arxiv.org/abs/1301.3485">Bordes <em>et al.</em> (2014)</a> alongside the
<a href="https://pykeen.readthedocs.io/en/latest/api/pykeen.models.UnstructuredModel.html">Unstructured Model</a>.
<a href="https://www.aclweb.org/anthology/W15-4007">Toutanova and Chen (2015)</a> outlined
an algorithm for removing leakage due to inverse triples and applied it to
respectively derive two new datasets: the Freebase 15K-237 (FB15k-237) dataset
and the WordNet-18 (RR; WN-18RR) dataset.</p>

<table>
  <thead>
    <tr>
      <th>Dataset</th>
      <th style="text-align: right">Entities</th>
      <th style="text-align: right">Relations</th>
      <th style="text-align: right">Triples</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>WN-18</td>
      <td style="text-align: right">40,943</td>
      <td style="text-align: right">18</td>
      <td style="text-align: right">151,442</td>
    </tr>
    <tr>
      <td>WN-18RR</td>
      <td style="text-align: right">40,559</td>
      <td style="text-align: right">11</td>
      <td style="text-align: right">92,583</td>
    </tr>
    <tr>
      <td>FB15k</td>
      <td style="text-align: right">14,951</td>
      <td style="text-align: right">1,345</td>
      <td style="text-align: right">592,213</td>
    </tr>
    <tr>
      <td>FB15k-237</td>
      <td style="text-align: right">14,505</td>
      <td style="text-align: right">237</td>
      <td style="text-align: right">310,079</td>
    </tr>
  </tbody>
</table>

<p>These datasets are of roughly the same size and are widely available. Therefore,
if I read a paper that presents results on only FB15k and WN-18 but not
FB15k-237 and WN-18RR, I am concerned that the authors either do not know about
this problem, or are intentionally misleading the reader.</p>

<p>An alternative kind of testing leakage can occur in chemoinformatic
applications. The following three chemical structures are from a structural
class called <a href="https://identifiers.org/CHEBI:87631">statins (CHEBI:87631)</a> that
inhibit the HMG-CoA reductase enzyme and are often used to lower blood pressure
to reduce the risk of heart disease.</p>

<table>
  <thead>
    <tr>
      <th><a href="https://identifiers.org/CHEBI:34848">mevastatin (CHEBI:34848)</a></th>
      <th><a href="https://identifiers.org/CHEBI:40303">lovastatin (CHEBI:40303)</a></th>
      <th><a href="https://identifiers.org/CHEBI:9150">simvastatin (CHEBI:9150)</a></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><img src="https://www.ebi.ac.uk/chebi/displayImage.do?defaultImage=true&amp;imageIndex=0&amp;chebiId=34848" alt="mevastatin" /></td>
      <td><img src="https://www.ebi.ac.uk/chebi/displayImage.do?defaultImage=true&amp;imageIndex=0&amp;chebiId=40303" alt="lovastatin" /></td>
      <td><img src="https://www.ebi.ac.uk/chebi/displayImage.do?defaultImage=true&amp;imageIndex=0&amp;chebiId=9150" alt="simvastatin" /></td>
    </tr>
  </tbody>
</table>

<p>All three share a stereospecific cyclic lactone motif (top right part of the
molecule) that result in their functional activity. If you’re building a
classifier for activity against HMG-CoA reductase, or more generally, a link
prediction model that includes chemical-protein or chemical-enzyme class
predictions, including some of these molecules in the training set and others in
the test set would enable trivial inference and ultimately result in an
over-estimation of the performance of your model (barring discussion about how
this could be considered trivial is a huge triumph of modern pharmacology and
cheminformatics). The solution often referenced in
<a href="https://www.universiteitleiden.nl/en/research/research-projects/science/lacdr-proteochemometrics">proteochemometrics</a>
papers is to use scaffold-based splitting like Bemis-Murcko scaffold splitting
or to pre-cluster molecules based on structural similarity and to split based on
clusters. More information on these methods is reviewed in
<a href="https://www.sciencedirect.com/science/article/pii/S1740674920300111">Bongers <em>et al.</em> (2019)</a>
and
<a href="https://www.frontiersin.org/articles/10.3389/fmolb.2020.00093/full">Parks <em>et al.</em> (2020)</a>.</p>

<h3 id="redundant-information-in-graphs">Redundant Information in Graphs</h3>

<p>Transcriptomics experiments are often analyzed with statistical
over-representation analysis or
<a href="https://www.gsea-msigdb.org/gsea/index.jsp">gene set enrichment analysis (GSEA)</a>
with respect to gene sets derived from pathway databases like KEGG, Reactome,
WikiPathways, the Pathway Interaction Database, and BioCyc, functional
annotations such as those from the Gene Ontology, and biological signatures such
as those listed in MSigDB. In addition to <em>p</em>-values, the high number of
statistical tests also motivates the reporting of adjusted <em>p</em>-values to address
<a href="https://en.wikipedia.org/wiki/Multiple_comparisons_problem">multiple hypothesis testing</a>.
However, these adjustments do not consider the high overlap in information due
to the redundancy of pathway databases or potential correlations between
expert-defined pathways and certain functional or biological signatures.
Therefore, several results from the same theme or biological phenomena often
co-occur as enriched which often leads to an inflated sense of concordance of
results and thus higher confirmation bias. Further,
<a href="https://www.frontiersin.org/articles/10.3389/fgene.2019.01203/full">Mubeen <em>et al.</em> (2019)</a>
showed that the variability of definitions in each pathway database for the
“same” pathway (e.g., apoptosis) caused significantly different results in many
downstream tasks.</p>

<p>Many benchmark biological knowledge graphs such as <a href="https://het.io/">Hetionet</a>
and <a href="https://github.com/openbiolink/openbiolink">OpenBioLink</a> include multiple
pathway databases. Therefore, link prediction tasks between genes and pathways
(such as the case scenario presented in
<a href="https://doi.org/10.1093/bioinformatics/btz117">Ali <em>et al</em>., 2019</a>) could be
skewed both during the training and evaluation of link prediction models, and
ultimately in the interpretation of predicted results. While methodological
improvements like SetRank
(<a href="https://bmcbioinformatics.biomedcentral.com/articles/10.1186/s12859-017-1571-6">Simillion <em>et al.</em>, 2017</a>)
have been proposed to mitigate the redundancies in the over-representation
analysis setting, there have not yet been methodological improvements for link
prediction models that consider these kinds redundancies. Further, these kinds
of redundancies do not fit as neatly into the “leakage” issue.</p>

<p>Tools like <a href="https://github.com/compath/">ComPath</a> can be used to enrich KGs with
links between functionally equivalent pathways, but this kind of information is
not readily used by typical knowledge graph embedding models. Alternative
rule-based and symbolic reasoning systems might present solutions in this area.</p>

<h3 id="the-effect-of-non-determinism">The Effect of Non-Determinism</h3>

<p>There are several meaningful non-deterministic factors that affect the
performance of a model:</p>

<ol>
  <li>The effect of random initialization. The commonly used <code class="language-plaintext highlighter-rouge">pytorch.nn.Embedding</code>
initializes its weightings by default with the vanilla
<code class="language-plaintext highlighter-rouge">pytorch.nn.init.normal_</code> function. Using alternative parameters for the
distribution (e.g., Xavier/Glorot) have been shown to improve performance in
a variety of settings.</li>
  <li>The effect of non-determinism during training that occurs during batch
shuffling. This has shown to quicken convergence and mitigate overfitting
based on the ordering of training data.</li>
  <li>The effect of random negative sampling when training under the stochastic
local closed world (sLCWA) assumption.</li>
</ol>

<p>To compensate for the non-determinism, several trials using the same settings
should be run and the distribution of each metric (or at minimum, summary
statistics over their means and standard deviations) should be presented. Below,
the distribution of mean ranks resulting from 10 trials of training of several
models on the Hetionet dataset are presented (credit to Stephen Bonner
<a href="https://github.com/sbonner0">@sbonner</a> for running this trial and generating
the chart).</p>

<p><img src="/img/bonner_rand_init.png" alt="The effect of random initialization" /></p>

<p>From this chart, it is obvious that the
<a href="https://pykeen.readthedocs.io/en/latest/api/pykeen.models.DistMult.html">DistMult</a>
model has the potential to perform both very well and very poorly based on the
random initialization. There are two reasons that this might be the case for
this model/dataset in particular:</p>

<ol>
  <li>The interaction for DistMult function uses the Hadamard operators, which
commute, so <code class="language-plaintext highlighter-rouge">f(h,r,t) = f(t,r,h)</code>. With the Hetionet dataset, where there is
meaningful directionality and also the existence of inverse edges, this could
be a liability to both meaningful training and accurate evaluation</li>
  <li>The L_1 norm formulation of DistMult makes it much more susceptible to random
initialization than if the L_2 norm were specified. Because the original
formulation of the DistMult model uses the idiom of matrix factorization, the
norm’s <code class="language-plaintext highlighter-rouge">p</code> is not configurable within PyKEEN (the tool used to train these
models and generate these charts).</li>
</ol>

<p>While there are a few insights into the formulations of models and the
properties of datasets that could inform users to avoid using models like
DistMult with datasets like Hetionet, the more general case is likely more
bleak - even small changes in hyper-parameters could potentially affect the
robustness of a model to non-determinism. However, there does not yet exist a
comprehensive benchmarking study (even over small KGs) to confirm this nor
elucidate general insight.</p>

<p>As an end note - this is not meant to say that non-determinism should be
mitigated nor removed. It is a vital part to creating meaningful results and
fixing the random seed is certainly not a viable solution.</p>

<h3 id="splits-arent-gospel">Splits Aren’t Gospel</h3>

<p>Benchmark datasets for knowledge graph embedding models typically consist of a
pre-stratified training, testing, and validation set. The FB15k, FB15k-237,
WN18, WN18-RR, and YAGO3-10 are the most commonly used to assess the performance
of new models or compare the performance of old models due to their relatively
small sizes. More specifically, the same exact pre-stratification is used in all
papers.</p>

<p>While this potentially could make results more comparable across different
experiments, it presents a huge liability in that it is another “random” effect
that is completely discarded. Rather than present results on a single split,
results should be presented as a distribution over several splits to show if
there are certain properties of the split that cause some models to perform
better than others. Perhaps this kind of evaluation would have informed Bordes
<em>et al.</em> about the issues with the FB15k and WN18 datasets before they were
later identified by other authors. This also presents a practical problem since
there are potentially many ways to generate dataset splits, and typical
knowledge graph embedding model researchers likely aren’t interested in adding
another level of cross-validation to their already complex pipelines.</p>

<p>This issue is more apparent when applying knowledge graph embedding models to
datasets that are not pre-stratified such as the
<a href="http://arxiv.org/abs/2012.11490">Common Sense Knowledge Graph</a>,
<a href="http://rtw.ml.cmu.edu/rtw/">NELL</a>, or any biomedical knowledge graph. It can
again be demonstrated with another image from Stephen Bonner. Below, the
distribution of mean ranks resulting from 10 random splits of the Hetionet
dataset and using the same training configuration for several models.</p>

<p><img src="/img/bonner_rand_splits.png" alt="The effect of random splits" /></p>

<p>The confidence intervals support an even more startling conclusion - the effect
of random splitting of the dataset could cause a model like DistMult to
outperform models like ComplEx due to its huge variance. Further, datasets like
Hetionet definitely are susceptible to the leakage problem. Note: there’s a
prototype implementation of the Toutanova and Chen algorithm for removing
leakages in PyKEEN, but it’s neither finished nor integrated in the PyKEEN
pipeline. Combine with DistMult’s penchant for modeling inverse relations the
same as the canonical ones, this could wreak havoc on its performance depending
on how bad the leakage was by chance. Like with random initialization, there has
not yet been a comprehensive benchmarking that could give more general insight
into the liabilities of not reporting distributions of results over several
random splits. Even further, combine with the previous section’s prescription to
report distribution over multiple random initializations, doing a “proper”
investigating of robustness seems to be getting more high dimensional.</p>

<h2 id="do-kgems-actually-learn">Do KGEMs Actually Learn?</h2>

<p>The formulation of the link prediction task on knowledge graphs as a binary
classification task over a dataset of true positives and randomly sampled
negatives (under the local closed world assumption) is already a concession to
many more obvious machine learning tasks (like classifying Mendel’s irises).
Even further, many machine learning techniques have the concept of a null model
against which they can compare if their results are meaningful, or just good by
chance. In classification tasks, this is often done with y-scrambling which
compares the evaluation metric obtained from the real dataset to the evaluation
metrics obtained by repetitively shuffling the response variable and generating
a null distribution of evaluation metrics against which the real one can be
compared. Ultimately, the <em>p</em>-value of a one-tailed test against this
distribution tells you if you actually learned something, or if your results are
the same as what you’d get with a random dataset.</p>

<p>While this is a pretty typical test that reviewers ask for in other machine
learning tasks, it has not (yet) penetrated the knowledge graph embedding model
domain. I think this likely limited by the difficulty in writing performant
research code. While tools like PyTorch BigGraph and Amazon’s DGL are able to
scale across many machines or GPUs, they are not accessible to anyone without
significant dev-ops abilities nor are they frameworks that could support
researchers. PyKEEN itself has the opposite problem (for now, get hyped for new
improvements in 2021). A solution might lie in
<a href="https://github.com/marius-team/marius">Marius</a>, a new knowledge graph embedding
model library that could be the best of both worlds. Assuming these weren’t
considerations, there are two themes through which I could see the question “did
I learn anything?”: knowledge graph reorganization and knowledge graph
deterioration.</p>

<p>The idea of reorganization is a bit more classic, coming from previous
approaches. There are a few concepts that could be used to generate derived
knowledge graphs based on a given one that could be use to generate null
networks:</p>

<ol>
  <li>Degree-preserving network shuffle
(<a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3307026">pmc:PMC3307026</a>;
<a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5168876">pmc:PMC5168876</a>)</li>
  <li>Xswap (<a href="https://doi.org/10.1137/1.9781611972795.67">paper</a>;
<a href="https://github.com/hetio/xswap">code</a>)</li>
  <li>Relabeling of nodes, similarly to the y-scrambling technique</li>
</ol>

<p><a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5640425/">Himmelstein <em>et al</em> (2017)</a>
provides a gold standard on how these techniques could be used to report on how
meaningful results are from a link prediction model on a knowledge graph.</p>

<p>The idea of deterioration encompasses randomly removing edges from the knowledge
graph and training. It could determine how much of knowledge graph is actually
needed to train the model to make meaningful prediction. This would be done for
multiple increments (e.g., remove 10% of the training set, remove 20% of the
training set) with multiple trials on each.</p>

<h3 id="are-kgems-king-now">Are KGEMs King Now?</h3>

<p>The COVID-19 pandemic has not only been a battle for public health, but it has
also resulted in the largest wave of low quality scientific work in recent
memory. Lots of this low-quality work included half-baked usage of network
representation learning, matrix completion, and for the first time, knowledge
graph embedding models in the mainstream to predict drug-disease links. It was
lacking on many fronts: the formulations of knowledge graphs, the evaluation of
training and link prediction, and the communication of results.</p>

<p>As a critique, I will share with you my own drug repositioning algorithm for
COVID-19: take the list of all drugs in DrugBank, and randomly select 100 of
them. You’d be surprised how competitive this algorithm is with others.</p>

<p>The more upsetting examples was from a <em>very</em> prominent network science group
that used the RotatE (and other standard) knowledge graph embedding models for
the link prediction task for drug repositioning for COVID-19. They evaluated the
task on its ability to recover drugs that had already gone into clinical trials
for COVID-19, and claimed victory because they were able to recapitulate enough
of the list. This was a bad evaluation because the drugs in the clinic were
there based on inference made on the same data used to train and evaluate the
model. More importantly, it was bad because it was a famous group that
misrepresented the way science is done - link prediction in biomedicine is a way
to generate hypotheses that can be tested in the lab. Focusing communication
around its ability to recapitulate what’s in the training set (i.e.,
overfitting) is one of the first lessons that students are taught to avoid in
their introduction to machine learning classes.</p>

<p>Does this mean that knowledge graph embedding models are king now? No - there’s
still a long way to go in biomedicine before they are accepted as canonical
tools, that must include some more high-profile papers in that include
experimental evaluation of the results. This will most likely happen in the
chemistry or target prioritization domains, where the predictions of the model
can motivate experiments of actual value.</p>

<p>As a few of the vignettes in this post mentioned, there are still problems with
model formulation that can cause huge performance deficits on biomedical
knowledge graphs. A different class of models based on random walks, while less
complex than knowledge graph embedding models, continue to perform well on tasks
in biomedical knowledge graphs because their formulation is oriented towards
extracting local community structure - a feature that is very important in
biology. Several attempts have been made to extend these to better take into
account directionality and edge type. Some of those generalizations even fit
into the philosophy and architecture of graph neural networks (GNNs). Further,
there are lots of quantitative data such as chemical-enzyme IC<sub>50</sub>
values, chemical-receptor EC<sub>50</sub> values, GWAS <em>p</em>-values, etc. that all
have widely different scales and dynamic ranges that are difficult to
simultaneously incorporate in any of these methods in a meaningful way.</p>

<h3 id="willkommen-in-der-zukunft-the-future">Willkommen in der Zukunft (The Future)</h3>

<p>Random parting thoughts:</p>

<p>Biomedical knowledge graph benchmarks are consistently topping out around 5
million edges. If I had to guess, that’s because that’s the number where if you
go higher, you have to start using some packages that are not meant for users
like Amazon’s DGL or PyTorch’s BigGraph. We’ll need more accessible, scalable
systems if we’re ever going to move on from drug repositioning, where there are
only on the scale of thousands of chemicals against hundreds of proven targets,
to <em>bona fide</em> drug discovery, where there are on the scale of billions of
chemicals (or more if you want to get fancy with virtual screening) against
potentially the whole genome of ~20K protein coding genes, the products of other
non-protein-coding genes, and other stuff floating around in and around the
cell.</p>

<p>Negative knowledge should be emphasized. Formats like the
<a href="https://biological-expression-language.github.io/">Biological Expression Language (BEL)</a>
should support its curation, natural language processing systems and tools like
<a href="https://github.com/sorgerlab/indra">INDRA</a> should start extracting in bulk, and
the concept of the open world assumption, local closed world assumption, and
closed world assumption need to be amended to better reflect the reality that we
know some negative knowledge, but it’s distributed across different relations in
different densities.</p>

<p>Genes, RNAs, and proteins are not the same! Even worse, they can have several
variants due to differential splicing, and they can have all sorts of pre- and
post-translational modifications. BEL and related formats like BioPAX can
represent these, but the knowledge graph approach to biology tends to throw this
away completely. On the other hand, high granular information on this level of
specificity is likely too low to make dense knowledge graphs over which typical
tasks like link prediction can be successful. This motivates a greater focus on
modeling the hierarchies of information (a task at which INDRA excels) to
generate knowledge graphs that balance these competing qualities.</p>

<p>The entire semantic infrastructure for building knowledge graphs is improving
(I’d like to say in some small part due to the tools I’ve built) that enables
mappings between entities, such as <a href="/2020/04/19/inspector-javerts-xref-database.html">Inspector Javert’s Xref
database</a> and the new
<a href="https://github.com/mapping-commons/SSSOM">SSSOM format</a> to make mapping data
more standardized.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="bioinformatics" /><category term="pykeen" /><summary type="html"><![CDATA[After many discussions scientists from AstraZeneca’s knowledge graph and target prioritization platform (BIKG) about the PyKEEN knowledge graph embedding model package, I joined them in writing a review on biomedical knowledge graphs. I’m giving a talk in their group tomorrow - this blog post is a longer form of some ideas I’ll be presenting there. Here are the slides.]]></summary></entry><entry><title type="html">Explaining MCI Conversion with Path Queries to NeuroMMSig</title><link href="https://cthoyt.com/2021/02/23/cpath-visit.html" rel="alternate" type="text/html" title="Explaining MCI Conversion with Path Queries to NeuroMMSig" /><published>2021-02-23T22:53:00+00:00</published><updated>2021-02-23T22:53:00+00:00</updated><id>https://cthoyt.com/2021/02/23/cpath-visit</id><content type="html" xml:base="https://cthoyt.com/2021/02/23/cpath-visit.html"><![CDATA[<p>In late 2017, I visited the <a href="https://c-path.org/">Critical Path Institute</a> in
Tucson, Arizona with my colleague Daniel Domingo-Fernández to use our
<a href="https://neurommsig.scai.fraunhofer.de/">Alzheimer’s disease map</a> encoded in the
<a href="https://biological-expression-language.github.io/">Biological Expression Language (BEL)</a>
and the tools we built with <a href="https://github.com/pybel/pybel">PyBEL</a> to help
contextualize their mild cognitive impairment (MCI) conversion models. We got
very interesting results, but they had a major overlap with unpublished work of
one of our colleagues on the role of
<a href="https://identifiers.org/hgnc:24565">KANSL1</a> in Alzheimer’s disease, so we never
reported them. Last week, his <a href="https://doi.org/10.3233/JAD-201397">paper</a>
finally made it publication (congratulations, Sepehr!) so I thought it would be
fun to rehash the old results and look at how the results might have changed
over time with improvements to the underlying knowledge graph.</p>

<p>After a long flight from Germany and layover in Phoenix (remember before the
pandemic when that was a thing?), we were received at the Tucson Airport by
<a href="https://www.linkedin.com/in/klaus-romero-66356844/">Klaus Romero</a>, the Director
of Quantitative Medicine at the Critical Path Institute (C-Path). He helped us
acclimatized to the quiet, flat plains of the Sonoran Desert with a ride down
its long stretches of highways in his BMW V8 on our way to our accommodations in
the city.</p>

<p><img src="/img/tucson_cacti.jpg" alt="Cacti outside the Tucson Airport" /></p>

<p>We were lucky to have arrived at C-Path when we did - Klaus’s team was mostly
remote, but met in person once or twice a year. They were the mavericks of the
institute - the team of computational biologists, pharmacologists, and
toxicologists who took advantage of the deep ties of the institute to regulatory
bodies like the American Food and Drug Administration (FDA) to pilot some of the
first computational tools to gain regulatory approval for use in a clinical
setting.</p>

<p>As our working lay on the computational side of neurodegeneration, we were
introduced to
<a href="https://www.linkedin.com/in/daniela-conrado-82492945/">Daniela Conrado</a> and her
work on a clinical data-driven conversion model from mild cognitive impairment
(MCI) to full Alzheimer’s disease. Ultimately, it was a linear mixed-effect
model that implicated several clinical exams and measurements as co-variates:</p>

<ul>
  <li>Clinical Dementia Rating (CDR)</li>
  <li>Clinical Dementia Rating Scale (sum of boxes) (CDR-SOB)</li>
  <li>Mini Mental State Exam (MMSE)</li>
  <li>APOE ε4 status</li>
  <li>beta amyloid 40</li>
  <li>beta amyloid 42</li>
  <li>hippocampal volume</li>
  <li>hippocampal atrophy</li>
</ul>

<p>It became our goal to use NeuroMMSig (the Alzheimer’s disease map) to identify a
potentially explanatory sub-graph (i.e., a mechanism) that connected all the
co-variates. We were well-positioned by using a disease map encoded in BEL that
allows for the inclusion of multi-scale knowledge across the molecular, pathway,
and phenotypic levels in addition to correlative and associative relationships
that aren’t so easy (or possible) with other modeling formalisms like BioPAX,
SBML, SBGN, Cell Designer, GO-CAM, etc. After spending months re-curating the
NeuroMMSig subgraph following the end of the AETIONOMY project, we were excited
to apply it and all of its shiny new goodness. What comes below is a slightly
edited version of the report we wrote at the end of our visit, hence its more
scientific voice (as opposed to my light-hearted blogging voice).</p>

<h2 id="methodology">Methodology</h2>

<p>When we first mapped the co-variates in the MCI conversion model to nodes in our
knowledge graph, we found some had high information density, like the amyloid
and APOE nodes, but the clinical measurements were relatively low. While this
would generally lead to poor robustness, an advantage of curated knowledge
graph-based approaches is that we were able to curate additional relevant
knowledge for these nodes by making use of automated and semi-automated relation
extraction pipelines as well as manual curation for enrichment. Further, the
clinical relationships were curated during the work presented in
<a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5611802/">NIFT project</a>, so we
were able to adapt the curation guidelines and apply them once again. We did
some curation with the help of master’s student of molecular biology Lukas
Beniusis who was working in our group back in Germany as a student research
assistant at the time. Ultimately, the resulting curated content found its way
to GitHub in the
<a href="https://github.com/pharmacome/conib/blob/master/hbp_knowledge/biomarkers/cdr_sb_associations.bel">CONIB</a>
repository under the CC BY 4.0 license. After substantially broadening the
connections with other nodes, more hypotheses could be generated in algorithmic
steps.</p>

<p>Graph traversal algorithms were used to find paths connecting the co-variates in
the Alzheimer’s disease map that had the appropriate polarity (a series of
increases, decreases, positive correlations, and/or negative correlations that
matched the observations of the MCI model). These paths were combined to
sub-graphs that could be interpreted and so individual paths could be
prioritized by their explanatory potential. Finally, the sub-graphs were
filtered to best rationalize the MCI model with the relevant molecular biology.
All of these steps were done in a local instance of
<a href="https://github.com/bel-commons/bel-commons">BEL Commons</a> that was loaded with
the NeuroMMSig BEL file.</p>

<p><img src="/img/cpath_road.jpg" alt="C-Path Results" /></p>

<p>We didn’t spend the <em>entire</em> time in Arizona working. We also got to run in a
dried up river bed, play Frogger trying to cross the street, and eat Klaus and
his wife’s amazing cooking.</p>

<h2 id="results">Results</h2>

<p>The most interesting hypotheses are presented in the graph below. The first
hypothesis (yellow) involves three entities classically associated with AD: the
amyloid precursor protein (APP); one of its secretases (BACE1); and the amyloid
beta 42 (Aβ-42) proteolytic cleavage fragment from the cleavage of APP by BACE1.
They participate in a positive feedback loop where an increase in the abundance
of the secretase leads to more amyloid beta aggregation. Since this process is
widely described in the literature, there are multiple connections, or
crosstalks, from this part of the graph to the more novel paths we describe in
the following.</p>

<p><img src="/img/cpath_graph.png" alt="C-Path Results" /></p>

<h3 id="rs2732703">rs2732703</h3>

<p>The second hypothesis (red) focuses on the possible effects of the
<a href="https://www.ncbi.nlm.nih.gov/snp/rs2732703">rs2732703</a> variant in the KANSL1
gene, which has been statistically linked to the hippocampus, and indirectly to
hippocampal volume. The gene product of KANSL1 forms an acetylation complex with
KAT8 that regulates the expression of genes involved in the oxidative
phosphorylation localized in the mitochondria. Therefore, dysregulation of the
transcription of the KANSL1 gene due to this variant might lead to oxidative
stress and ultimately to inflammation — one of the main processes associated
with AD.</p>

<p>There may be a crosstalk between APP and KAT8 since the APP intracellular domain
(AICD) has been observed to form a ternary complex with FE65 and KAT5. Because
KAT5 and KAT8 both share the domain that mediates the formation of this complex
(histone acetyltransferase domain, MYST-type ( IPR002717)), we suggest that KAT8
could act through the same mechanism as KAT5 to regulate the expression of
multiple genes (Müller <em>et al.</em>, 2013).</p>

<h3 id="dyrk1a">DYRK1A</h3>

<p>The third hypothesis (green) suggested by the subgraph involves DYRK1A (dual
specificity tyrosine-phosphorylation-regulated kinase 1A) which is negatively
correlated with the Clinical Dementia Rating Scale - Sum of Boxes (CDR-SoB), a
cognitive score included in the clinical MCI model. This kinase phosphorylates
two proteins deposited in the brain that are hallmarks of AD: tau protein (MAPT)
and APP (Wegiel <em>et al.</em>, 2011). While hyperphosphorylation of the Tau protein
causes the aggregation of neurofibrillary tangles, phosphorylation of APP
increases its cleavage leading to higher levels of beta-amyloid. Furthermore,
DYRK1A acts as a regulator of apoptotic processes and neuronal death, which
might result in hippocampus atrophy (Choi &amp; Chung, 2011).</p>

<h3 id="serpine1">SERPINE1</h3>

<p>The final hypothesis (blue) is predicated by the positive correlations between
SERPINE1 abundance and two co-variates from the MCI model: the CDR-SoB and the
mini mental state exam (MMSE) (Oh <em>et al.</em>, 2014). These correlations motivate
further curation that could provide more causal insight as well as investigation
of measurements of this protein as a biomarker for these clinical endpoints.</p>

<h2 id="afterthoughts">Afterthoughts</h2>

<p>Our findings highlighted crosstalk between several well-known mechanisms (e.g.,
beta-amyloid, oxidative stress, and inflammation) and the co-variates observed
by the MCI model. Furthermore, the analysis identified various controllers that
might regulate these processes: rs2732703, DYRK1A, and SERPINE1 that may be
appropriate to investigate in a clinical setting.</p>

<p>However, knowledge-based approaches are inherently incomplete, and are not a
replacement for hypothesis-driven experiments or clinical trials. Even in our
analysis, we were not able to account for some co-variants, such as sex, because
of the nature of the data structure for storing knowledge and the granularity of
available knowledge itself. Interpreting hypotheses generated by a purely
knowledge-driven environment also requires great care. In this exercise, we
postponed exploration of another classic co-variate, the APOE mutations due to
this constraint.</p>

<p>Additional knowledge enrichment around the clinical endpoints used in MCI models
could be used to provide further support for these hypotheses as well as
facilitate the generation of new hypotheses. Back in 2017, we wrote that future
work could include expansion of the graph around the genes of interest with
dedicated literature mining approaches as well as enriching the graph with
further knowledge around the co-variates. I mentioned Sepehr’s paper
<a href="https://doi.org/10.3233/JAD-201397">A Systems Biology Approach for Hypothesizing the Effect of Genetic Variants on Neuroimaging Features in Alzheimer’s Disease</a>
at the onset in which he prioritized curation around the SNPs associated with
Alzheimer’s disease and related neurodegenerative diseases like Parkinson’s
disease through genome-wide association studies (GWASs). He also converged on
the KANSL1 gene through an orthogonal analysis after which he (luckily) did
orthogonal curation to ours to add additional content to NeuroMMSig than what we
did. In the years since this analysis, we also made huge improvements to PyBEL
and <a href="https://github.com/bel-commons/bel-commons">BEL Commons</a>, the interactive
visualization platform in which we did the analysis presented here that makes it
much more simple and flexible.</p>

<p>Despite several publications using the NeuroMMSig BEL content, none were ever
accompanied by a release of the underlying BEL content. In 2018, I began to lead
work on the <a href="https://pharmacome.github.io">Human Brain Pharmacome Project</a> which
included additional curation to enrich NeuroMMSig. I made sure that all of this
found its way directly into the open source. If/when the NeuroMMSig BEL content
is made publicly available, I will follow up this post with a very short script
showing how to do the following to reproduce the graph presented above:</p>

<ol>
  <li>Load a BEL graph</li>
  <li>Create a shortest paths query using the
<code class="language-plaintext highlighter-rouge">pybel.struct.get_subgraph_by_all_shortest_paths()</code> function</li>
  <li>Visualize the resulting subgraph in a Jupyter notebook.</li>
</ol>

<p>Until then, good luck with the next wave of (endless) beta amyloid antibodies!</p>

<h2 id="references">References</h2>

<ul>
  <li>Choi, H. K., &amp; Chung, K. C. (2011). Dyrk1A positively stimulates ASK1-JNK
signaling pathway during apoptotic cell death. Experimental neurobiology,
20(1), 35-44.</li>
  <li>Domingo-Fernández, D., <em>et al.</em> (2017). Multimodal Mechanistic Signatures for
Neurodegenerative Diseases (NeuroMMSig): a web server for mechanism
enrichment. Bioinformatics, 33(22), 3679-3681.</li>
  <li>Müller, T., <em>et al.</em> (2013). A ternary complex consisting of AICD, FE65, and
TIP60 down-regulates Stathmin1. Biochimica et Biophysica Acta (BBA)-Proteins
and Proteomics, 1834(1), 387-394.</li>
  <li>Oh, J., Lee, H. J., Song, J. H., Park, S. I., &amp; Kim, H. (2014). Plasminogen
activator inhibitor-1 as an early potential diagnostic marker for Alzheimer’s
disease. Experimental gerontology, 60, 87-91.</li>
  <li>Wegiel, J., Gong, C. X., &amp; Hwang, Y. W. (2011). The role of DYRK1A in
neurodegenerative diseases. The FEBS journal, 278(2), 236-245.</li>
</ul>

<h3 id="subgraph-reference-pmids">Subgraph Reference PMIDs</h3>

<ul>
  <li>rs2732703: 25778476, 27768893, 21034527, 22544363, 27768893, 23092889</li>
  <li>DYRK1A: 21156028, 22110360, 25116835</li>
  <li>SERPINE1: 25304332</li>
</ul>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="curation" /><category term="bioinformatics" /><summary type="html"><![CDATA[In late 2017, I visited the Critical Path Institute in Tucson, Arizona with my colleague Daniel Domingo-Fernández to use our Alzheimer’s disease map encoded in the Biological Expression Language (BEL) and the tools we built with PyBEL to help contextualize their mild cognitive impairment (MCI) conversion models. We got very interesting results, but they had a major overlap with unpublished work of one of our colleagues on the role of KANSL1 in Alzheimer’s disease, so we never reported them. Last week, his paper finally made it publication (congratulations, Sepehr!) so I thought it would be fun to rehash the old results and look at how the results might have changed over time with improvements to the underlying knowledge graph.]]></summary></entry><entry><title type="html">Adding Structured Data to Docstrings</title><link href="https://cthoyt.com/2021/02/20/docdata.html" rel="alternate" type="text/html" title="Adding Structured Data to Docstrings" /><published>2021-02-20T20:12:00+00:00</published><updated>2021-02-20T20:12:00+00:00</updated><id>https://cthoyt.com/2021/02/20/docdata</id><content type="html" xml:base="https://cthoyt.com/2021/02/20/docdata.html"><![CDATA[<p>Writing excellent documentation is crucial for open source software projects.
It’s also a <em>lot</em> of hard work. While I consider tools like
<a href="https://www.sphinx-doc.org">Sphinx</a> combine with services like
<a href="https://readthedocs.org/">ReadTheDocs</a> completely invaluable, I’ve recently hit
a bit of a roadblock when it comes to making the README of a GitHub repository a
bit more dynamic. This blog post is about the dark magic I invented as a
solution (i.e., the <a href="https://github. com/cthoyt/docdata/"><code class="language-plaintext highlighter-rouge">docdata</code></a> package).</p>

<h2 id="how-python-documentation-works">How Python Documentation Works</h2>

<p>Before beginning, I want to give a quick refresher on what documentation looks
like in Python. For any class or object, you can write a docstring using
triple-double quotes on the first line after the definition</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">sin</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
    <span class="s">"""Compute the sin of x."""</span>
    <span class="p">...</span>


<span class="k">class</span> <span class="nc">MyClass</span><span class="p">:</span>
    <span class="s">"""This is my class."""</span>
    <span class="p">...</span>
</code></pre></div></div>

<p>It turns out that this is a bit more extensible than I thought. While the
triple-double quoted string is the community standard, you can also get away
with using triple-single quote, single quote, or double quote as well. Even
crazier, you can introduce a blank line before the docstring. While these are
possible, please don’t do this. There’s another interesting feature about the
docstring that makes it different from any old string sitting in Python code -
it’s not evaluated. This means that if you use an expression that isn’t a string
literal, it won’t be set to the docstring. The following code illustrates this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_docstr</span><span class="p">():</span>
    <span class="k">return</span> <span class="s">"""this is my docstr"""</span>


<span class="k">def</span> <span class="nf">my_func</span><span class="p">():</span>
    <span class="n">get_docstr</span><span class="p">()</span>
    <span class="k">return</span> <span class="mi">5</span>


<span class="k">assert</span> <span class="n">my_func</span><span class="p">.</span><span class="n">__doc__</span> <span class="ow">is</span> <span class="bp">None</span>
<span class="k">assert</span> <span class="n">my_func</span><span class="p">()</span> <span class="o">==</span> <span class="mi">5</span>
</code></pre></div></div>

<p>Not only are docstrings useful for readers of your code, but Python considers
them with great respect. <a href="https://www.python.org/dev/peps/pep-0257">PEP 257</a>
outlines in full detail, but the important thing to keep in mind is that Python
code can introspect on the docstring for any function, class, etc. with the
special <code class="language-plaintext highlighter-rouge">__doc__</code> attribute (e.g., <code class="language-plaintext highlighter-rouge">MyClass.__doc__</code> stores the docstring
itself). This feature is what enables tools like Sphinx to exist without having
to write an entirely new parser for Python files.</p>

<h2 id="a-tale-of-two-readmes">A Tale of Two READMEs</h2>

<p>The rest of this journey will be told through the perspective of my work on the
documentation of <a href="https://github.com/pykeen/pykeen/">PyKEEN</a>, a machine learning
library for learning low-dimensional embeddings for nodes and edges in knowledge
graphs. In this blog post, you don’t need to understand anything about the
package itself other than it has several types of interchangeable components
that can be combined to create a <em>model</em> that gets trained on a <em>dataset</em>. Its
documentation uses the
<a href="https://sphinx-automodapi.readthedocs.io"><code class="language-plaintext highlighter-rouge">sphinx-automodapi</code></a> extension to
generate pretty lists of all the datasets, models, loss functions, regularizers,
etc.
(<a href="https://pykeen.readthedocs.io/en/stable/reference/regularizers.html">example</a>).</p>

<p>The problem is that most people start to use a given package by either looking
at the README file in the GitHub repository, or the splash text on the PyPI
project page (which, for PyKEEN and most packages, is created with the README on
upload). I wanted to generate beautiful tables describing the components on the
README file the same as in the Sphinx documentation, so I started by writing a
template markdown file using <a href="https://jinja.palletsprojects.com"><code class="language-plaintext highlighter-rouge">jinja</code></a> as a
templating language. For each type of component, I programatically built a
table, formatted it as markdown with
<a href="https://pypi.org/project/tabulate/"><code class="language-plaintext highlighter-rouge">tabulate</code></a>, and formatted it into the
template.</p>

<p>The tricky part was making these tables better than just lists of the names of
the classes. Sphinx has a deep integration with the restructured text (RST)
format and provides custom “directives” like <code class="language-plaintext highlighter-rouge">:class:</code> that allow for automatic
linking between documentation for modules, classes, functions, variables, or
anything else. Luckily, the <code class="language-plaintext highlighter-rouge">sphinx-automodapi</code> uses a standard format for its
documentation. For example, the
<a href="https://pykeen.readthedocs.io/en/stable/api/pykeen.models.ComplEx.html"><code class="language-plaintext highlighter-rouge">pykeen.models.ComplEx</code></a>
class gets built with a URL like
<a href="https://pykeen.readthedocs.io/en/stable/api/pykeen.models.ComplEx.html">https://pykeen.readthedocs.io/en/stable/api/pykeen.models.ComplEx.html</a>
. The general form for <code class="language-plaintext highlighter-rouge">&lt;X&gt;</code> is
<code class="language-plaintext highlighter-rouge">https://pykeen.readthedocs.io/en/stable/api/&lt;X&gt;.html</code>. I was able to take
advantage of this and generate a column with the name of the class in PyKEEN
with a link to the documentation for the class on ReadTheDocs. This also gives
insight to users who might want to import these classes themselves.</p>

<p>The next tricky part was providing some context besides just the name and class
name. For new users looking at the models in PyKEEN, it’s also useful to show a
citation. This typically includes the first author’s last name and the year as
in “Ali <em>et al.</em>, 2019”. Additionally, the citation should link to the paper
itself for further reading past what the PyKEEN documentation for the model
provides. As an aside, it’s one of my personal goals for PyKEEN’s documentation
to be an educational resource that in many cases will be more useful than
reading manuscripts written by computer scientists, whose goals are to make
themselves smart more than to motivate and educate the reader. In RST, there’s a
<a href="https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#citations">syntax</a>
for linking citations that the PyKEEN documentation organizes in a
<a href="https://pykeen.readthedocs.io/en/stable/references.html">documentation-wide bibliography</a>.
Unfortunately, my templating system is not as powerful as Sphinx, and does not
parse all of these files. The solution I had was to standardize the citation
keys and the format of the first line of each model’s docstring such that the
name and year could be extracted with some simple text processing because I
enforced the standard that all model docstrings ended with the RST citation.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pykeen.models</span> <span class="kn">import</span> <span class="n">ComplEx</span>

<span class="n">doc</span> <span class="o">=</span> <span class="n">ComplEx</span><span class="p">.</span><span class="n">__doc__</span>
<span class="n">doc_lines</span> <span class="o">=</span> <span class="n">doc</span><span class="p">.</span><span class="n">splitlines</span><span class="p">()</span>
<span class="n">line</span> <span class="o">=</span> <span class="n">doc_lines</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>  <span class="c1"># get the first line of the docstring
</span><span class="n">l</span><span class="p">,</span> <span class="n">r</span> <span class="o">=</span> <span class="n">line</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="s">'['</span><span class="p">),</span> <span class="n">line</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="s">']'</span><span class="p">)</span>
<span class="n">author</span><span class="p">,</span> <span class="n">year</span> <span class="o">=</span> <span class="n">line</span><span class="p">[</span><span class="mi">1</span> <span class="o">+</span> <span class="n">l</span><span class="p">:</span> <span class="n">r</span> <span class="o">-</span> <span class="mi">4</span><span class="p">],</span> <span class="n">line</span><span class="p">[</span><span class="n">r</span> <span class="o">-</span> <span class="mi">4</span><span class="p">:</span> <span class="n">r</span><span class="p">]</span>
</code></pre></div></div>

<p>Then the author and year could be formatted into a new column in the previous
format. However, programatically getting the citation link was a completely
different problem. One solution I considered was to start adding class variables
with this information, but that would quickly become a distraction to users.</p>

<h2 id="my-solution">My Solution</h2>

<p>Then, I remembered a cool feature of
<a href="https://github.com/flasgger/flasgger/"><code class="language-plaintext highlighter-rouge">flasgger</code></a>, which automatically
generates a Swagger interface for Flask applications by embedding the Swagger
definition as YAML inside each
<a href="https://github.com/flasgger/flasgger/#using-docstrings-as-specification">route’s docstring</a>.
I didn’t look into their code for an implementation and tried it my own with
some pretty fast success! The simple (but robust) code that I wrote for PyKEEN
seemed generally useful, so I moved it into its own package
<a href="https://github.com/cthoyt/docdata/"><code class="language-plaintext highlighter-rouge">docdata</code></a> - so others could easily use it.
It went very fast because I recently put a lot of effort into creating a
<a href="https://github.com/cthoyt/cookiecutter-python-package">Cookiecutter package</a>
with all of my favorite settings that I’ve covered in previous posts on this
blog.</p>

<p>It does the same thing as <code class="language-plaintext highlighter-rouge">flasgger</code> - it allows the final few lines following a
delimiter string (i.e., <code class="language-plaintext highlighter-rouge">---</code>) of a docstring to be parsed as YAML and stored in
the object. It can be applied as a decorator to functions and classes, or simply
to any data or Python object that has a docstring. A demonstration shows it all:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">docdata</span> <span class="kn">import</span> <span class="n">parse_docdata</span><span class="p">,</span> <span class="n">get_docdata</span>


<span class="o">@</span><span class="n">parse_docdata</span>
<span class="k">class</span> <span class="nc">MyClass</span><span class="p">:</span>
    <span class="s">"""This is my class.

    ---
    author: Charlie
    motto:
    - docs
    - are
    - cool
    """</span>


<span class="k">assert</span> <span class="n">get_docdata</span><span class="p">(</span><span class="n">MyClass</span><span class="p">)</span> <span class="o">==</span> <span class="p">{</span>
    <span class="s">'author'</span><span class="p">:</span> <span class="s">'Charlie'</span><span class="p">,</span>
    <span class="s">'motto'</span><span class="p">:</span> <span class="p">[</span><span class="s">'docs'</span><span class="p">,</span> <span class="s">'are'</span><span class="p">,</span> <span class="s">'cool'</span><span class="p">],</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The data can also be accessed directly via <code class="language-plaintext highlighter-rouge">MyClass.__docdata__</code> but dunders are
scary, and I thought it would be nice to have a getter as well.</p>

<p>This was immediately useful for PyKEEN models because I was able to store all
the citation information in a structured way at the bottom of the docstring.
Then, I was able to improve my table generator to make a much more rich column
for citations that included the link to each. I also did something for datasets
in PyKEEN, but additionally included statistics about each dataset’s entities,
relations, and triples to make the PyKEEN README even more useful. The full pull
request on PyKEEN can be found at
<a href="https://github.com/pykeen/pykeen/pull/303">pykeen/pykeen!303</a>.</p>

<hr />

<p>If you’re interested in the philosophy of documentation, a good place to start
is <a href="https://www.youtube.com/watch?v=azf6yzuJt54">here</a>, or any other talk given
by Daniele Procida.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="Python" /><summary type="html"><![CDATA[Writing excellent documentation is crucial for open source software projects. It’s also a lot of hard work. While I consider tools like Sphinx combine with services like ReadTheDocs completely invaluable, I’ve recently hit a bit of a roadblock when it comes to making the README of a GitHub repository a bit more dynamic. This blog post is about the dark magic I invented as a solution (i.e., the docdata package).]]></summary></entry><entry><title type="html">Adding New Literature Sources to the Wikidata Integrator</title><link href="https://cthoyt.com/2021/01/23/updating-the-wikidata-integrator.html" rel="alternate" type="text/html" title="Adding New Literature Sources to the Wikidata Integrator" /><published>2021-01-23T20:12:00+00:00</published><updated>2021-01-23T20:12:00+00:00</updated><id>https://cthoyt.com/2021/01/23/updating-the-wikidata-integrator</id><content type="html" xml:base="https://cthoyt.com/2021/01/23/updating-the-wikidata-integrator.html"><![CDATA[<p><a href="https://scholia.toolforge.org">Scholia</a> is a powerful frontend for summarizing
authors, publications, institutions, topics, etc. that draws content from
<a href="https://www.wikidata.org">Wikidata</a>. However, the content that’s available in
Wikidata depends on what has been manually curated by community members and what
has been (semi-) automatically imported by scripts and bots. The
<a href="https://github.com/SuLab/WikidataIntegrator">Wikidata Integrator</a> from the
<a href="https://sulab.org">Su Lab</a> at Scripps automates the import of bibliometric
information from <a href="https://www.crossref.org/">Crossref</a> and
<a href="https://europepmc.org/">Europe PMC</a>. This blog post is about how I added
functionality to it to import from three prominent preprint servers in the
natural sciences (<a href="https://arxiv.org/">arXiv</a>,
<a href="https://www.biorxiv.org/">bioRxiv</a>, and <a href="https://chemrxiv.org/">ChemRxiv</a>) that
can serve as a guide to others who want to have content about their field
included with this tool.</p>

<p>A while back, I wanted to put an arXiv paper about
<a href="https://arxiv.org/abs/2006.08589">the role of metadata in reproducible computational research</a>
that I co-authored with Jeremy Leipzig
(<a href="https://twitter.com/jermdemo">@jermdemo</a>) on Wikidata so it would appear on
<a href="https://scholia.toolforge.org/author/Q47475003">my Scholia page</a>, but I didn’t
want to do it by hand. I had already had good experiences using Magnus Manske’s
<a href="https://sourcemd.toolforge.org/">SourceMD</a>,
<a href="https://author-disambiguator.toolforge.org">Author Disambiguator</a>, and related
tools for queueing import of peer-reviewed papers with PubMed identifiers or
digital object identifiers (DOIs), ORCID identifiers of co-authors, and
ultimately assisted disambiguation of author names.</p>

<p>The only problem was that the SourceMD tool doesn’t deal with preprints. As an
aside, both bioRxiv and ChemRxiv assign DOIs to their preprints that can be
queried through Crossref, but this doesn’t exactly solve the problem of handling
preprints in a special way, and it doesn’t solve the problem for arXiv. I asked
around on Twitter and was turned towards the Wikidata Integrator project, which
already supported building both general pipelines for automating content import
in Wikidata and a specific one for publications. All I had to do was figure out
how it works, and get hacking! I ended up sending the following three pull
requests:</p>

<table>
  <thead>
    <tr>
      <th>Server</th>
      <th>Pull Request</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>arXiv</td>
      <td><a href="https://github.com/SuLab/WikidataIntegrator/pull/140">SuLab/WikidataIntegrator#140</a></td>
    </tr>
    <tr>
      <td>bioRxiv</td>
      <td><a href="https://github.com/SuLab/WikidataIntegrator/pull/169">SuLab/WikidataIntegrator#169</a></td>
    </tr>
    <tr>
      <td>ChemRxiv</td>
      <td><a href="https://github.com/SuLab/WikidataIntegrator/pull/170">SuLab/WikidataIntegrator#140</a></td>
    </tr>
  </tbody>
</table>

<p>At the time of writing, the arXiv and bioRxiv ones have been accepted, and the
ChemRxiv one is waiting for review - I think the maintainer was busy at a
conference this week, and now I’m writing on a Saturday. In another unrelated
pull request to the project, I added a vanity CLI, so when you install the code
with <code class="language-plaintext highlighter-rouge">pip install wikidataintegrator</code>, a program
<code class="language-plaintext highlighter-rouge">wikidataintegrator-publication</code> is installed for direct usage from the shell.
It can be used like this:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>wikidataintegrator-publication <span class="nt">--idtype</span> arxiv 2101.05136
<span class="nv">$ </span>wikidataintegrator-publication <span class="nt">--idtype</span> biorxiv 2020.08.20.259226
<span class="nv">$ </span>wikidataintegrator-publication <span class="nt">--idtype</span> chemrxiv 13607438
</code></pre></div></div>

<p>This resulted in the following three Wikidata pages:</p>

<ul>
  <li>arXiv: <a href="https://www.wikidata.org/wiki/Q104846171">Q104846171</a></li>
  <li>bioRxiv: <a href="https://www.wikidata.org/wiki/Q104920313">Q104920313</a></li>
  <li>ChemRxiv: <a href="https://www.wikidata.org/wiki/Q104931192">Q104931192</a></li>
</ul>

<p>If you’re using a DOI or PubMed identifier as the <code class="language-plaintext highlighter-rouge">--idtype</code>, you also have to
specify a <code class="language-plaintext highlighter-rouge">--source</code>, but since each of arXiv, bioRxiv, and ChemRxiv have their
own custom sources, this isn’t necessary. The program will print the Wikidata
identifier (starting with a Q followed by some numbers) of the newly created
item, or an error message if there was a problem. It’s quite smart and avoids
creating duplicate pages by checking if the ID type has already been used with a
pre-defined Wikidata property that goes with it. More on that below in the
tutorial, since we have to make that definition when adding new sources.</p>

<h2 id="implementing-a-new-importer">Implementing a New Importer</h2>

<p>Luckily, all the work in implementing a new importer happens in one python
module:
<a href="https://github.com/SuLab/WikidataIntegrator/blob/main/wikidataintegrator/wdi_helpers/publication.py"><code class="language-plaintext highlighter-rouge">wikidataintegrator.wdi_helpers.publication</code></a>
. You can begin by clicking the
<a href="https://github.com/SuLab/WikidataIntegrator/edit/main/wikidataintegrator/wdi_helpers/publication.py">edit</a>
button, which will automatically fork the repository and create a new branch.
I’m terrible using git and managing multiple remotes, so this is my preferred
way to start a PR in any repository that I might want to PR more than once.</p>

<h3 id="adding-the-right-metadata">Adding the right metadata</h3>

<p><img src="/img/wdi_steps/1_2.png" alt="Wikidata Integrator - Add New Source - Steps 1 and 2" /></p>

<p>The first step is to identify the Wikidata property corresponding to entries in
your new source. This was already the case for arXiv
(<a href="https://www.wikidata.org/wiki/Property:P818">P818</a>) and bioRxiv
(<a href="https://www.wikidata.org/wiki/Property:P3951">P3951</a>), so I added an entry to
the <code class="language-plaintext highlighter-rouge">ID_TYPES</code> dictionary in the <code class="language-plaintext highlighter-rouge">Publication</code> class where the key is the name
of the source (not necessarily the name on the property, keep it short and
simple).</p>

<p>The second step is to identify the Wikidata item corresponding to the source
itself and add it to the <code class="language-plaintext highlighter-rouge">SOURCES</code> dictionary in the <code class="language-plaintext highlighter-rouge">Publication</code> class (just
below the <code class="language-plaintext highlighter-rouge">ID_TYPES</code> dictionary).</p>

<h3 id="implementing-the-getter">Implementing the getter</h3>

<p><img src="/img/wdi_steps/3.png" alt="Wikidata Integrator - Add New Source - Step 3" /></p>

<p>The third step is where the domain logic about your source comes in. You need to
implement a function that takes in an identifier that spits back an instance of
the <code class="language-plaintext highlighter-rouge">Publication</code> class. It also has to have an <code class="language-plaintext highlighter-rouge">id_type</code> argument where the
default value matches to the key you used in the sources. This actually isn’t
used anywhere, but must be there because of the interface that consumes it. This
function can live towards the bottom of the Python file and isn’t inside a
class. It’s best to put it next to the bioRxiv, arXiv, and Crossref ones.</p>

<p>Most of the way you get data from your source is up to you. Most sources have
some kind of endpoint that can be queried and returns JSON - note that Wikidata
Integrator has a consistent USER AGENT that tells services what kind of code is
querying it. This is important if you’re hitting an API with many queries so
sysadmins can see what’s going on.</p>

<p>The Publication class is pretty self-explanatory except for a few parts.</p>

<ol>
  <li>The <code class="language-plaintext highlighter-rouge">authors</code> keyword arguments takes a list of dictionaries whose keys are
<code class="language-plaintext highlighter-rouge">full_name</code> and <code class="language-plaintext highlighter-rouge">orcid</code>. You can omit <code class="language-plaintext highlighter-rouge">orcid</code> or pass <code class="language-plaintext highlighter-rouge">None</code>. bioRxiv isn’t
currently providing author information like ORCID identifiers in its API, so
it did not show up in this example, but you can see in the other parts of the
code for PMC and PubMed how this works.</li>
  <li>Make sure that the keys in the <code class="language-plaintext highlighter-rouge">ids</code> keyword argument correspond to keys in
<code class="language-plaintext highlighter-rouge">ID_TYPES</code>. If your document has more than one ID, you can put them her
(though there is some debate whether this is a good idea).</li>
  <li>You have to set the <code class="language-plaintext highlighter-rouge">instance_of</code> property not with the <code class="language-plaintext highlighter-rouge">__init__()</code> because
it gets handled with a Python descriptor. This is a design choice in the
Publication class that is out of our hands.</li>
  <li>Don’t forget to set the <code class="language-plaintext highlighter-rouge">published_in_qid</code> which will correspond to the
<code class="language-plaintext highlighter-rouge">venue</code> on your wikidata item.</li>
</ol>

<h3 id="tying-it-all-together">Tying it all together</h3>

<p>In the fourth step, you need to register your newly implemented function in the
<code class="language-plaintext highlighter-rouge">SOURCE_FUNCT</code> dictionary inside the <code class="language-plaintext highlighter-rouge">PublicationHelper</code> class. Make sure the
key you use is consistent with before, and use the function as the value. Don’t
use parentheses, since you want to actually have the function be the value and
not the result of calling the function. This is part of a programming paradigm
called “functional programming”.</p>

<p><img src="/img/wdi_steps/4.png" alt="Wikidata Integrator - Add New Source - Step 4" /></p>

<p>In the fifth and final step, you need to scroll up to the <code class="language-plaintext highlighter-rouge">get_or_create()</code>
function in the <code class="language-plaintext highlighter-rouge">Publication</code> class to tell it which ID is the primary key for
your item. Add a new conditional <code class="language-plaintext highlighter-rouge">elif self.source == 'your key'</code> to check for
your source key (same as all the other places you added it) then add the key
corresponding to your source from the ids (this was the part from step 3) and
the correct property. The previous examples use <code class="language-plaintext highlighter-rouge">PROPS</code> which is redundant. You
can use <code class="language-plaintext highlighter-rouge">self.ID_TYPES['your key']</code>.</p>

<p><img src="/img/wdi_steps/5.png" alt="Wikidata Integrator - Add New Source - Step 5" /></p>

<h3 id="testing">Testing</h3>

<p>Try running the following code with a valid ID and hope that everything works!
If you are having issues here, then you can always send a draft pull request to
solicit help from the maintainers of the project.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>wikidataintegrator-publication <span class="nt">--idtype</span> <span class="s2">"your key"</span> <span class="s2">"your id"</span>
</code></pre></div></div>

<p>Be careful here, since this will hit the live Wikidata instance. If you make a
new item that has a problem, please try to fix it since deleting entries from
Wikidata isn’t so common, and we don’t want to add to the mess!</p>

<h2 id="my-source-doesnt-have-a-wikidata-property">My Source Doesn’t Have a Wikidata Property</h2>

<p>In the case of ChemRxiv, DOIs are available for each article, so I did not need
to add a new entry to <code class="language-plaintext highlighter-rouge">ID_TYPES</code> dictionary. However, scholarly articles on
Wikidata typically use the DOI to point to the peer-reviewed article, and a
preprint-specific property to point to the preprint describing the same paper (I
know, confusing…).</p>

<p>I created a
<a href="https://www.wikidata.org/wiki/Wikidata:Property_proposal/ChemRxiv_ID">property proposal for “ChemRxiv ID”</a>
on Wikidata to help rectify this. You can propose a new property from
<a href="https://www.wikidata.org/wiki/Wikidata:Property_proposal/Generic">this page</a>
but beware: Wikidata property maintainers are quite cautious to add new things
and aren’t necessarily giving the most prompt feedback.</p>

<hr />

<p>I had a lot of fun working on this new codebase and this blog post, and it was a
reminder of the nice discussion I had with Andrew Su last year. I hope this post
enables others to add support for medRxiv, Preprints.org, and other places where
people are leaving their pre-prints!</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="bibliometrics" /><category term="Wikidata" /><summary type="html"><![CDATA[Scholia is a powerful frontend for summarizing authors, publications, institutions, topics, etc. that draws content from Wikidata. However, the content that’s available in Wikidata depends on what has been manually curated by community members and what has been (semi-) automatically imported by scripts and bots. The Wikidata Integrator from the Su Lab at Scripps automates the import of bibliometric information from Crossref and Europe PMC. This blog post is about how I added functionality to it to import from three prominent preprint servers in the natural sciences (arXiv, bioRxiv, and ChemRxiv) that can serve as a guide to others who want to have content about their field included with this tool.]]></summary></entry><entry><title type="html">Organizing the Public Data about your Research Organization</title><link href="https://cthoyt.com/2021/01/17/organization-organization.html" rel="alternate" type="text/html" title="Organizing the Public Data about your Research Organization" /><published>2021-01-17T18:23:00+00:00</published><updated>2021-01-17T18:23:00+00:00</updated><id>https://cthoyt.com/2021/01/17/organization-organization</id><content type="html" xml:base="https://cthoyt.com/2021/01/17/organization-organization.html"><![CDATA[<p>If you’ve ever read a scientific paper, you know that the information that makes
it into the author affiliations is a mess. I’m a big fan of
<a href="https://manubot.org/">Manubot</a> and fully support its mission to upend the
modern scientific publishing model. Like how they use structured
<a href="https://orcid.org/">ORCID identifiers</a> for identifying authors in manuscript
metadata, they are also working towards using <a href="https://ror.org">ROR identifiers</a>
for organizations. There are still a few growing pains for ROR, so I chimed in
on a <a href="https://github.com/manubot/manubot/issues/155">discussion</a> on GitHub about
how Wikidata might be a potential solution for organizing and retrieving
information about reserach organizations. I said I’d describe my idea more in
detail, so here I go!</p>

<h2 id="step-1-wikidata">Step 1: Wikidata</h2>

<p><a href="https://www.wikidata.org">Wikidata</a> is an open, community-curated platform of
knowledge. It stores entities, their relations to other entities, their
relations to scalar values, and added context for each relationship. Typically,
relationships have a subject, relation, and object and can be read like a simple
sentence in the english language.</p>

<p>There are lots of working groups that maintain its ontology (i.e., the rules for
how curation should be done) around certain domains, such representing
organization structures. This means there are lots of tools already built in to
Wikidata for potential curators like you and me to create rich pages for their
organizations.</p>

<p>One of the curation rules shared across all domains in Wikidata is that each
entity should have a “type”. This means on the page for
<a href="https://www.wikidata.org/wiki/Q937">Albert Einstein</a>, there is a relationship
stating he is an <a href="https://www.wikidata.org/wiki/Property:P31">instance of</a> a
<a href="https://www.wikidata.org/wiki/Q5">human</a>. The “instance of” item on Wikidata is
a special kind called a “property” and is one of the places where the ontology
lives - there are specific rules for each property on how it should be used in a
relationship, like what’s allowed to be the subject and what’s allowed to be the
object. For”instance of”, there are no rules about the subject. However, the
object of the relationship where “instance of” is the property should be a
“class” of thing. It wouldn’t make sense for the type of another entity to be an
instance of “Albert Einstein”.</p>

<h3 id="ontology-for-organizations">Ontology for Organizations</h3>

<p>When making a Wikidata page for an organization, it should have a type of either
<a href="https://www.wikidata.org/wiki/Q43229">organization</a> or one of its subclasses
representing various kinds of organizations, parts of organizations, or
combinations of subclasses and parts. Some examples are:</p>

<ul>
  <li><a href="https://www.wikidata.org/wiki/Q28863779">university research group</a> like the
<a href="https://www.wikidata.org/wiki/Q107380113">Laboratory of Systems Pharmacology</a></li>
  <li><a href="https://www.wikidata.org/wiki/Q31855">research institute</a> like the
<a href="https://www.wikidata.org/wiki/Q1451981">Fraunhofer SCAI</a></li>
  <li><a href="https://www.wikidata.org/wiki/Q2366457">department</a> like
<a href="https://www.wikidata.org/wiki/Q67200492">Fraunhofer SCAI Department of Bioinformatics</a></li>
  <li><a href="https://www.wikidata.org/wiki/Q3918">university</a> like
<a href="https://www.wikidata.org/wiki/Q37548">Northeastern University</a></li>
  <li><a href="https://www.wikidata.org/wiki/Q180958">faculty</a> like the
<a href="https://www.wikidata.org/wiki/Q48888910">Maastricht Faculty of Health, Medicine and Life Sciences</a></li>
  <li><a href="https://www.wikidata.org/wiki/Q2467461">academic department</a> like
<a href="https://www.wikidata.org/wiki/Q19845644">Maastricht University Department of Bioinformatics (BiGCaT)</a></li>
  <li><a href="https://www.wikidata.org/wiki/Q4830453">business</a> like
<a href="https://www.wikidata.org/wiki/Q206921">Pfizer</a></li>
</ul>

<p>Typically, organizations have certain pieces of information associated with them
using the following properties:</p>

<ul>
  <li><a href="https://www.wikidata.org/wiki/Property:P1813">short name</a> - is there a short
version of your organization’s name? BiGCaT is again a good example.</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P17">country</a> - in which country is
your organization based? Wikidata already has items for countries, so the
autocomplete will help you fill out this very easily.</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P856">official website</a> - what is the
URL of the homepage for your organization?</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P361">part of</a> and
<a href="https://www.wikidata.org/wiki/Property:P527">has part</a> - what are the parent
organization(s) and child organizations? If your item is a department, it is
likely “part of” a school, faculty, college, university, or research
institution. It’s best to be as specific as possible. If you are describing a
department that is part of a faculty in a university (see the BiGCaT example
above), it doesn’t make sense to be redundant and also write that it is a part
of the University.</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P101">field of work</a> - what topics do
your organization work on / research? This can be things like cancer, machine
learning, etc.</li>
</ul>

<p>Leadership and organizer links:</p>

<ul>
  <li><a href="https://www.wikidata.org/wiki/Property:P488">chairperson</a> - who is the leader
of the organization? This person should themselves have a Wikidata page. For a
department, this is equivalent to the department chair/head.</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P169">chief executive officer</a> - who
is the CEO of a business?</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P112">founded by</a> - who founded a
business?</li>
  <li><a href="https://www.wikidata.org/wiki/Property:P1037">director / manager</a> - who is
the PI of the lab?</li>
</ul>

<p>External Account Links:</p>

<ul>
  <li><a href="https://www.wikidata.org/wiki/Property:P2002">Twitter username</a></li>
  <li><a href="https://www.wikidata.org/wiki/Property:P4264">LinkedIn company ID</a></li>
  <li><a href="https://www.wikidata.org/wiki/Property:P2037">GitHub username</a></li>
</ul>

<h3 id="tutorial">Tutorial</h3>

<ol>
  <li>Create a <a href="https://www.wikidata.org/wiki/Special:NewItem">new item</a>. You don’t
need to have a Wikidata account or be logged in, but there are lots of
benefits, so that’s highly suggested through
<a href="https://www.wikidata.org/w/index.php?title=Special:CreateAccount&amp;returnto=Wikidata%3AMain+Page">this portal</a>.</li>
  <li>Input the name for the item. If you’re making a page for the Northeastern
Department of Chemistry, it makes most sense to include the name of the
parent organization inside the label for the item. There are some instances
where this isn’t the case, such as the example above of the Maastricht
Faculty of Health, Medicine and Life Sciences, but this could cause
confusion. The rest of the form is pretty obvious, but if you aren’t sure
what to entry for the description, writing what kind of thing it is might be
best. For the Northeastern Department of Chemistry, one might write “academic
department” <img src="/img/wikidata_create_item.png" alt="Wikidata create item page" /></li>
  <li>You’ll see a mostly blank page. Start by clicking the “+ add statement” link
in the middle to bring up the following box. On the left where “Property” is
greyed out, you can type the name of the relationship and click the right
suggestion from the dropdown
<img src="/img/wikidata_add_statement_1.png" alt="Wikidata add statement step 1" /></li>
  <li>In the middle, you can type the object of the relationship. If the property
accepts another Wikidata item, it will show an autocomplete field. If the
property accepts a scalar (like an identifier), then no dropdown will pop up.
<img src="/img/wikidata_add_statement_2.png" alt="Wikidata add statement step 2" /></li>
  <li>After checking what you’ve typed is correct, click publish! Don’t worry, all
items can be edited/updated later.
<img src="/img/wikidata_add_statement_3.png" alt="Wikidata add statement step 3" /></li>
  <li>Repeat for all information you know about the organization. It’s a bit
high-minded, but you can also follow the links for the properties mentioned
above to see the rules they define for how they should be used. The cool
thing about ontologies is that properties can actually be the subject of
relationships that describe how they work using “meta-properties” 🤯.</li>
</ol>

<p>If you’ve got any suggestions for improving this tutorial, feel free to get in
touch with me (contact information at the bottom of this post) or make a PR to
this page directly on GitHub!</p>

<h2 id="step-2-research-organization-registry">Step 2: Research Organization Registry</h2>

<p>The <a href="https://ror.org">Research Organization Registry (ROR)</a> is an organization
dedicated to assigning unique identifiers to all research organizations.
Definitely check out their homepage, I couldn’t write it any better than they
did. They can accomplish the same goal that you can on Wikidata, but there’s a
clear advantage towards having nomenclature authorities that manually curate and
maintain data.</p>

<p>Their <a href="https://ror.org/curation/">curation page</a> has a link to their request
form where you can send information about your organization to them. Even
better, they accept Wikidata identifiers, so you don’t have to type everything
all over again! I personally filled out the form and had some very nice email
exchanges later with their team, so I hope you have a nice experience too.
Eventually, they will assign your organization a ROR identifier as well as
curate relationships between your organization and others.</p>

<p>Finally, the whole loop that started with Wikidata can be closed by copying your
newly minted ROR identifier back to your organization’s Wikidata item. Wikidata
has a property <a href="https://www.wikidata.org/wiki/Property:P6782">ROR ID</a> whose
object is the scalar identifier from ROR!</p>

<h2 id="step-3-optional-but-cool-things">Step 3: Optional but Cool Things</h2>

<p>Wikidata, ROR, and GRID (below) are by far the best solutions for storing
scholarly metadata because of their licensing and API accessibility. Ringgold
and ISNI are not good options for building a reusable infrastructure since they
are both closed and paid systems, but they are still relevant (until the
revolution) for some aspects of modern scientific publication.</p>

<h3 id="global-research-identifier-database">Global Research Identifier Database</h3>

<p>The <a href="https://grid.ac/">Global Research Identifier Database (GRID)</a> is a parallel
effort that imports data from ROR on a quarterly (every three months) basis. It
assigns an additional GRID identifier to an organization that is already in ROR.
GRID is released about every three months via FigShare under the
<a href="https://creativecommons.org/publicdomain/zero/1.0/">Creative Commons Public Domain 1.0 International license</a>,
which means anyone can use it any way they want.</p>

<p>Like with ROR, GRID has a Wikidata property
<a href="https://www.wikidata.org/wiki/Property:P2427">GRID ID</a> that links a Wikidata
item to the scalar GRID identifier.</p>

<p><strong>August 10, 2021 update</strong>: GRID is shutting down and getting consumed by ROR
(<a href="https://www.digital-science.com/grid-passes-the-torch-to-ror-faqs/">ref</a>).</p>

<h3 id="ringgold-and-isni">Ringgold and ISNI</h3>

<p>Many publishers use the Ringgold system for organization resolution (such as
Manuscript Central). For a group interested in scholarly publishing (which could
either be an academic or commercial organization), you can simultaneously apply
for a Ringgold and ISNI by emailing isni@ringgold.com (copied from their
<a href="https://www.ringgold.com/isni/">FAQ</a>).</p>

<p>These can be linked to a Wikidata item with the
<a href="https://www.wikidata.org/wiki/Property:P3500">Ringgold ID</a> and
<a href="https://www.wikidata.org/wiki/Property:P213">ISNI</a> properties.</p>

<h3 id="scholia">Scholia</h3>

<p>Once your Wikidata page is full of information, you can use
<a href="https://scholia.toolforge.org/">Scholia</a> to visualize all the information
attached to your organization through Wikidata. For example, see the
<a href="https://scholia.toolforge.org/organization/Q35794">University of Cambridge</a>. To
get the most of this, you should annotate for each member of your organization
that they are affiliated, have been educated at, or have been employed at your
organization, whether it’s current or past - Wikidata has additional metadata
for a relationship for representing the time period during which it was true.</p>

<hr />

<p>Scholia shows off how easy it is to build a system on top of the data in
Wikidata and the (free!) SPARQL infrastructure they provide for accessing the
data however you want. This is the original reason for me bringing up Wikidata
in the Manubot issue tracker. It would be totally reasonable to hit their SPARQL
endpoint with one query per Manubot build and continue being good open source
citizens.</p>

<p>This blog post might have gone off on a couple tangents besides what was
relevant for Manubot, but because I did all of these things to set up the
internet presence of my previous employer, it could serve as a good guide for
any new organizations to get up to speed.</p>

<p>See also the <a href="/2021/08/17/self-organization.html">newer post</a> on
organizing all of the public data about a researcher, like you!</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="bibliometrics" /><summary type="html"><![CDATA[If you’ve ever read a scientific paper, you know that the information that makes it into the author affiliations is a mess. I’m a big fan of Manubot and fully support its mission to upend the modern scientific publishing model. Like how they use structured ORCID identifiers for identifying authors in manuscript metadata, they are also working towards using ROR identifiers for organizations. There are still a few growing pains for ROR, so I chimed in on a discussion on GitHub about how Wikidata might be a potential solution for organizing and retrieving information about reserach organizations. I said I’d describe my idea more in detail, so here I go!]]></summary></entry><entry><title type="html">How to Code with Me - Wrapping a Flask App in a CLI</title><link href="https://cthoyt.com/2021/01/11/click-and-flask.html" rel="alternate" type="text/html" title="How to Code with Me - Wrapping a Flask App in a CLI" /><published>2021-01-11T00:14:00+00:00</published><updated>2021-01-11T00:14:00+00:00</updated><id>https://cthoyt.com/2021/01/11/click-and-flask</id><content type="html" xml:base="https://cthoyt.com/2021/01/11/click-and-flask.html"><![CDATA[<p>Previous posts in my “How to Code with Me” series have addressed <a href="/2020/06/03/how-to-code-with-me-organization.html">packaging
python code</a> and
<a href="/2020/06/11/click.html">setting up a command line interface (CLI) using
<code class="language-plaintext highlighter-rouge">click</code></a>. This post is about how to do this
when your Python code is running a web application made with
<a href="https://flask.palletsprojects.com">Flask</a> and how to set it up to run through
your CLI.</p>

<p>The name of the package I’ll be referring to in this tutorial is
<code class="language-plaintext highlighter-rouge">granola_explosion</code> (not a real package!) that follows the <code class="language-plaintext highlighter-rouge">src/</code> layout. If
you’re not familiar with this, check my previous post on <a href="/2020/06/03/how-to-code-with-me-organization.html">organizing a Python
package</a>.</p>

<h2 id="example-flask-application">Example Flask Application</h2>

<p>Let’s assume that the Flask application is in a module called
<code class="language-plaintext highlighter-rouge">granola_explosion.wsgi</code> located at <code class="language-plaintext highlighter-rouge">src/granola_explosion/wsgi.py</code>. This
tutorial isn’t about building a Flask application, so below I’ll give a minimum
working example. Your Flask application may be much larger, even spanning
multiple files. The important thing is that the
<a href="https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask"><code class="language-plaintext highlighter-rouge">flask.Flask</code></a>
instance is living in this file.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># wsgi.py
</span>
<span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span>

<span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>


<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">route</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">home</span><span class="p">():</span>
    <span class="k">return</span> <span class="s">"There's no place like home."</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">app</span><span class="p">.</span><span class="n">run</span><span class="p">()</span>
</code></pre></div></div>

<p>Note that the
<a href="https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask.run"><code class="language-plaintext highlighter-rouge">app.run()</code></a>
is enclosed in <code class="language-plaintext highlighter-rouge">if __name__ == '__main__'</code>. This means that the app only gets
run if the <code class="language-plaintext highlighter-rouge">granola_explosion.wsgi</code> is run as a script. Later, we will be
importing this module inside our CLI, and we don’t want it to run until we tell
it to (and with our very own options).</p>

<h2 id="run-a-flask-web-application-with-click">Run a Flask Web Application with Click</h2>

<p>Let’s also assume your command line interface is in a module called
<code class="language-plaintext highlighter-rouge">granola_explosion.cli</code> located at <code class="language-plaintext highlighter-rouge">src/granola_explosion/cli.py</code> using a
<a href="https://click.palletsprojects.com/en/7.x/api/#click.Group"><code class="language-plaintext highlighter-rouge">click.Group</code></a> to
organize several subcommands. The following example shows how you can import the
<code class="language-plaintext highlighter-rouge">app</code> object and run it from inside the command line.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># cli.py
</span><span class="kn">import</span> <span class="nn">click</span>


<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">group</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="s">"""Run the Granola Explosion CLI."""</span>


<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">command</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">web</span><span class="p">():</span>
    <span class="kn">from</span> <span class="nn">.wsgi</span> <span class="kn">import</span> <span class="n">app</span>
    <span class="n">app</span><span class="p">.</span><span class="n">run</span><span class="p">()</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<p>Now, you can run your web application with
<code class="language-plaintext highlighter-rouge">python -m granola_explosion.cli web</code>!</p>

<p>You can actually call your module and <code class="language-plaintext highlighter-rouge">flask.Flask</code> instance whatever you want,
but these two are pretty standard and recognized by external tools (more on that
later), and will make it easier for other people to understand what your code
does.</p>

<h2 id="configure-your-application">Configure Your Application</h2>

<p>Normally, you can pass options like <code class="language-plaintext highlighter-rouge">host</code> and <code class="language-plaintext highlighter-rouge">port</code> into the
<code class="language-plaintext highlighter-rouge">flask.Flask.run()</code> function. Below, we use <code class="language-plaintext highlighter-rouge">click</code> options to pass these
through.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># cli.py
</span><span class="kn">import</span> <span class="nn">click</span>


<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">group</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="s">"""Run the Granola Explosion CLI."""</span>


<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">command</span><span class="p">()</span>
<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">option</span><span class="p">(</span><span class="s">'--host'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s">'0.0.0.0'</span><span class="p">)</span>
<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">option</span><span class="p">(</span><span class="s">'--port'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">5000</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">web</span><span class="p">(</span><span class="n">host</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">port</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
    <span class="kn">from</span> <span class="nn">.wsgi</span> <span class="kn">import</span> <span class="n">app</span>
    <span class="n">app</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">host</span><span class="o">=</span><span class="n">host</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="n">port</span><span class="p">)</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<p>I’ve written these options so many times, that I made a package called
<a href="https://github.com/cthoyt/more_click/"><code class="language-plaintext highlighter-rouge">more_click</code></a> that holds them for easy
importing like in the following:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># cli.py
</span><span class="kn">import</span> <span class="nn">click</span>
<span class="kn">from</span> <span class="nn">more_click</span> <span class="kn">import</span> <span class="n">host_option</span><span class="p">,</span> <span class="n">port_option</span>


<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">group</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="s">"""Run the Granola Explosion CLI."""</span>


<span class="o">@</span><span class="n">main</span><span class="p">.</span><span class="n">command</span><span class="p">()</span>
<span class="o">@</span><span class="n">host_option</span>
<span class="o">@</span><span class="n">port_option</span>
<span class="k">def</span> <span class="nf">web</span><span class="p">(</span><span class="n">host</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">port</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="kn">from</span> <span class="nn">.wsgi</span> <span class="kn">import</span> <span class="n">app</span>
    <span class="n">app</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">host</span><span class="o">=</span><span class="n">host</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="n">port</span><span class="p">)</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="using-gunicorn">Using GUnicorn</h2>

<p>The <code class="language-plaintext highlighter-rouge">flask.Flask.run()</code> function is convenient, but it’s only meant to be a
lightweight development server - even your own server yells at you every time
you start it!</p>

<p><img src="/img/flask-development-warning.png" alt="Flask Development Warning" /></p>

<p>The
<a href="https://flask.palletsprojects.com/en/1.1.x/deploying/">official documentation</a>
and many excellent tutorials point to using more powerful servers like
<a href="https://gunicorn.org/">Gunicorn</a>, but they start throwing around the (scary)
acronym <a href="https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface">WSGI</a> and
tend to have very dense documentation that looks like it’s written only for
sysadmins.</p>

<p><img src="/img/flask-gunicorn-docs.png" alt="Flask Gunicorn Tutorial" /></p>

<p>If you’re like me, you’re a big fan of keeping as much code in Python as
possible, rather than floating around in various shell scripts and dockerfiles.
You would also probably like to be able to run a Flask app with Gunicorn with
the ease of <code class="language-plaintext highlighter-rouge">app.run()</code>.</p>

<p>It turns out that <code class="language-plaintext highlighter-rouge">gunicorn</code> is actually written in Python, and this is possible
if you’re willing to read through the codebase and understand the complicated
design patterns they use. Or, you could use the <code class="language-plaintext highlighter-rouge">more_click.run_app</code>, which
takes care of all of it for you. The implementation of this function lives
<a href="https://github.com/cthoyt/more_click/blob/main/src/more_click/web.py">here</a>,
for the adventurous among you. It’s basically a drop-in replacement for
<code class="language-plaintext highlighter-rouge">app.run()</code> except it’s called as <code class="language-plaintext highlighter-rouge">run_app(app)</code>. Then, you can use the
<code class="language-plaintext highlighter-rouge">with_gunicorn</code> keyword argument to turn on using Gunicorn.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># cli.py
</span><span class="kn">import</span> <span class="nn">click</span>
<span class="kn">from</span> <span class="nn">more_click</span> <span class="kn">import</span> <span class="n">host_option</span><span class="p">,</span> <span class="n">port_option</span><span class="p">,</span> <span class="n">run_app</span>


<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">group</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="s">"""Run the Granola Explosion CLI."""</span>


<span class="o">@</span><span class="n">main</span><span class="p">.</span><span class="n">command</span><span class="p">()</span>
<span class="o">@</span><span class="n">host_option</span>
<span class="o">@</span><span class="n">port_option</span>
<span class="k">def</span> <span class="nf">web</span><span class="p">(</span><span class="n">host</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">port</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="kn">from</span> <span class="nn">.wsgi</span> <span class="kn">import</span> <span class="n">app</span>
    <span class="n">run_app</span><span class="p">(</span><span class="n">app</span><span class="o">=</span><span class="n">app</span><span class="p">,</span> <span class="n">with_gunicorn</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">host</span><span class="o">=</span><span class="n">host</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="n">port</span><span class="p">)</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<p>Now, your app runs with Gunicorn! If you want to be able to quickly switch back
and forth between Flask and Gunicorn as a server, you can use the handy
<code class="language-plaintext highlighter-rouge">more_click.with_gunicorn_option</code>. Further, you can specify the number of
workers for your Gunicorn server based on the following complete example:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># cli.py
</span><span class="kn">import</span> <span class="nn">click</span>
<span class="kn">from</span> <span class="nn">more_click</span> <span class="kn">import</span> <span class="n">host_option</span><span class="p">,</span> <span class="n">port_option</span><span class="p">,</span> <span class="n">with_gunicorn_option</span><span class="p">,</span> <span class="n">workers_option</span><span class="p">,</span> <span class="n">run_app</span>


<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">group</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="s">"""Run the Granola Explosion CLI."""</span>


<span class="o">@</span><span class="n">main</span><span class="p">.</span><span class="n">command</span><span class="p">()</span>
<span class="o">@</span><span class="n">host_option</span>
<span class="o">@</span><span class="n">port_option</span>
<span class="o">@</span><span class="n">with_gunicorn_option</span>
<span class="o">@</span><span class="n">workers_option</span>
<span class="k">def</span> <span class="nf">web</span><span class="p">(</span><span class="n">host</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">port</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">with_gunicorn</span><span class="p">:</span> <span class="nb">bool</span><span class="p">,</span> <span class="n">workers</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
    <span class="kn">from</span> <span class="nn">.wsgi</span> <span class="kn">import</span> <span class="n">app</span>
    <span class="n">run_app</span><span class="p">(</span><span class="n">app</span><span class="o">=</span><span class="n">app</span><span class="p">,</span> <span class="n">with_gunicorn</span><span class="o">=</span><span class="n">with_gunicorn</span><span class="p">,</span> <span class="n">host</span><span class="o">=</span><span class="n">host</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="n">port</span><span class="p">,</span> <span class="n">workers</span><span class="o">=</span><span class="n">workers</span><span class="p">)</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="ultimate-lazy-mode">Ultimate Lazy Mode</h2>

<p>For ultimate lazy mode, I’ve written a wrapper around the complete example in
<code class="language-plaintext highlighter-rouge">more_click.make_web_command</code>. This uses a standard <code class="language-plaintext highlighter-rouge">wsgi</code>-style string to
locate the app. While this is a little less explicit than normal Python code
that relies on the import machinery, it has the benefit that it can lazily
import the module in which your Flask application lives. This could help avoid
importing big requirements, as well as allow your package to specify Flask
requirements as optional. You might want to do this if your package can be used
to perform a service locally, but also contains a Flask application that wraps
it with a RESTful service as well that not all users might need.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># cli.py
</span><span class="kn">import</span> <span class="nn">click</span>
<span class="kn">from</span> <span class="nn">more_click</span> <span class="kn">import</span> <span class="n">make_web_command</span>


<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">group</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="s">"""My awesome CLI."""</span>


<span class="n">make_web_command</span><span class="p">(</span><span class="s">'my_package_name.wsgi:app'</span><span class="p">,</span> <span class="n">group</span><span class="o">=</span><span class="n">main</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">make_web_command()</code> function actually returns the command itself, so you
can save it and add it to the group manually instead of passing the <code class="language-plaintext highlighter-rouge">group</code>
argument.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># cli.py
</span><span class="kn">import</span> <span class="nn">click</span>
<span class="kn">from</span> <span class="nn">more_click</span> <span class="kn">import</span> <span class="n">make_web_command</span>


<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">group</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="s">"""My awesome CLI."""</span>


<span class="n">web</span> <span class="o">=</span> <span class="n">make_web_command</span><span class="p">(</span><span class="s">'my_package_name.wsgi:app'</span><span class="p">)</span>

<span class="n">main</span><span class="p">.</span><span class="n">add_command</span><span class="p">(</span><span class="n">web</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<p>Since any click command can be run by itself directly, the following minimal CLI
also works well for apps that don’t need the click Group.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># cli.py
</span><span class="kn">from</span> <span class="nn">more_click</span> <span class="kn">import</span> <span class="n">make_web_command</span>

<span class="n">web</span> <span class="o">=</span> <span class="n">make_web_command</span><span class="p">(</span><span class="s">'granola_explosion.wsgi:app'</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">web</span><span class="p">()</span>
</code></pre></div></div>

<hr />

<p>I intentionally did not cover the built-in
<a href="https://flask.palletsprojects.com/en/1.1.x/cli/">Flask Script</a> because it
doesn’t fit in with my paradigm of “everything must be packaged.”</p>

<p>This is my first post of 2021! Happy new year!</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="code with me" /><summary type="html"><![CDATA[Previous posts in my “How to Code with Me” series have addressed packaging python code and setting up a command line interface (CLI) using click. This post is about how to do this when your Python code is running a web application made with Flask and how to set it up to run through your CLI.]]></summary></entry><entry><title type="html">Pathway Relationships</title><link href="https://cthoyt.com/2020/12/30/pathway-relationships.html" rel="alternate" type="text/html" title="Pathway Relationships" /><published>2020-12-30T08:00:00+00:00</published><updated>2020-12-30T08:00:00+00:00</updated><id>https://cthoyt.com/2020/12/30/pathway-relationships</id><content type="html" xml:base="https://cthoyt.com/2020/12/30/pathway-relationships.html"><![CDATA[<p>Domingo-Fernandez <em>et al.</em> published
<a href="https://doi.org/10.1038/s41540-018-0078-8">ComPath: An ecosystem for exploring, analyzing, and curating mappings across pathway databases.</a>
in 2018 describing the overlap between human pathways in KEGG, Reactome, and
WikiPathways. A lot of the underlying machinery I developed to support this
project has been improved since, and it’s time to spread the search to other
organisms besides humans and other databases. This blog post is about some
additional relation types needed to capture the relations between pathways
appearing in these databases.</p>

<p>Like many of my blog posts, this one was inspired by a tweet. After the
following discussion, I thought it would be good to better organize the ideas
and elaborate.</p>

<blockquote><p lang="en" dir="ltr"><a href="https://twitter.com/hashtag/KEGG">#KEGG</a> provides a namespace of non-species-specific pathway terms that all of its species-specific pathways can map to. <a href="https://twitter.com/reactome">@reactome</a> could you provide the same?</p>&mdash; Charles Tapley Hoyt (@cthoyt) <a href="https://twitter.com/cthoyt/status/1344076052386238464">December 30, 2020</a></blockquote>

<p>This blog post will follow apoptosis, one of the most ubiquitous pathways in
biology that covers all manners of programed cell death. This blog post isn’t
about the nitty-gritty difference between pathways, biological processes, and
mechanisms - so we will consider all variants of apoptosis and apoptotic process
effectively the same.</p>

<table>
  <thead>
    <tr>
      <th>Resource</th>
      <th>Prefix</th>
      <th>Identifier</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Gene Ontology (GO)</td>
      <td>go</td>
      <td><a href="https://identifiers.org/GO:0006915">GO:0006915</a></td>
    </tr>
    <tr>
      <td>Medical Subject Headings (MeSH)</td>
      <td>mesh</td>
      <td><a href="https://identifiers.org/mesh:D017209">D017209</a></td>
    </tr>
    <tr>
      <td>Kyoto Encyclopedia of Genes and Genomes (KEGG)</td>
      <td>kegg.pathway</td>
      <td><a href="https://identifiers.org/kegg.pathway:map04210">map04210</a></td>
    </tr>
    <tr>
      <td>NCI Thesaurus (NCIT)</td>
      <td>ncit</td>
      <td><a href="https://identifiers.org/ncit:C17557">C17557</a></td>
    </tr>
  </tbody>
</table>

<p>KEGG, Reactome, and WikiPathways all provide human-specific variants of these
pathways (below) as well as many other species, including both model organisms
and not.</p>

<table>
  <thead>
    <tr>
      <th>Resource</th>
      <th>Prefix</th>
      <th>Identifier</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>KEGG</td>
      <td>kegg.pathway</td>
      <td><a href="https://identifiers.org/kegg.pathway:hsa04210">hsa04210</a></td>
    </tr>
    <tr>
      <td>Reactome</td>
      <td>reactome</td>
      <td><a href="https://identifiers.org/reactome:R-HSA-109581">R-HSA-109581</a></td>
    </tr>
    <tr>
      <td>WikiPathways</td>
      <td>wikipathways</td>
      <td><a href="https://identifiers.org/wikipathways:WP254">WP254</a></td>
    </tr>
  </tbody>
</table>

<h2 id="pathways-are-equivalent">Pathways are Equivalent</h2>

<p>Two pathways are equivalent and can be represented with <code class="language-plaintext highlighter-rouge">skos:exactMatch</code> if
they both have the same species specificity. The following relationships are
between the non-species specific pathways for apoptosis:</p>

<table>
  <thead>
    <tr>
      <th>Subject</th>
      <th>Predicate</th>
      <th>Object</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://identifiers.org/GO:0006915">GO:0006915</a></td>
      <td>skos:exactMatch</td>
      <td><a href="https://identifiers.org/mesh:D017209">mesh:D017209</a></td>
    </tr>
    <tr>
      <td><a href="https://identifiers.org/GO:0006915">GO:0006915</a></td>
      <td>skos:exactMatch</td>
      <td><a href="https://identifiers.org/kegg.pathway:map04210">kegg.pathway:map04210</a></td>
    </tr>
    <tr>
      <td><a href="https://identifiers.org/mesh:D017209">mesh:D017209</a></td>
      <td>skos:exactMatch</td>
      <td><a href="https://identifiers.org/kegg.pathway:map04210">kegg.pathway:map04210</a></td>
    </tr>
    <tr>
      <td>…</td>
      <td>…</td>
      <td>…</td>
    </tr>
  </tbody>
</table>

<p>The following relationships are between the human-specific pathways for
apoptosis in KEGG, Reactome, and WikiPathways:</p>

<table>
  <thead>
    <tr>
      <th>Subject</th>
      <th>Predicate</th>
      <th>Object</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://identifiers.org/kegg.pathway:hsa04210">kegg.pathway:hsa04210</a></td>
      <td>skos:exactMatch</td>
      <td><a href="https://identifiers.org/reactome:R-HSA-109581">reactome:R-HSA-109581</a></td>
    </tr>
    <tr>
      <td><a href="https://identifiers.org/kegg.pathway:hsa04210">kegg.pathway:hsa04210</a></td>
      <td>skos:exactMatch</td>
      <td><a href="https://identifiers.org/wikipathways:WP254">wikipathways:WP254</a></td>
    </tr>
    <tr>
      <td><a href="https://identifiers.org/wikipathways:WP254">wikipathways:WP254</a></td>
      <td>skos:exactMatch</td>
      <td><a href="https://identifiers.org/reactome:R-HSA-109581">reactome:R-HSA-109581</a></td>
    </tr>
  </tbody>
</table>

<p>Similarly, the relationships between cow-specific (Bos Taurus; BTA) pathways for
apoptosis in KEGG, Reactome, and WikiPathways:</p>

<table>
  <thead>
    <tr>
      <th>Subject</th>
      <th>Predicate</th>
      <th>Object</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://identifiers.org/">kegg.pathway:bta04210</a></td>
      <td>skos:exactMatch</td>
      <td><a href="https://identifiers.org/">reactome:R-BTA-109581</a></td>
    </tr>
    <tr>
      <td><a href="https://identifiers.org/">kegg.pathway:bta04210</a></td>
      <td>skos:exactMatch</td>
      <td><a href="https://identifiers.org/">wikipathways:WP1018</a></td>
    </tr>
    <tr>
      <td><a href="https://identifiers.org/">wikipathways:WP1018</a></td>
      <td>skos:exactMatch</td>
      <td><a href="https://identifiers.org/">reactome:R-BTA-109581</a></td>
    </tr>
  </tbody>
</table>

<p>While equivalences begins to tame the ontology of pathways, it is missing links
between the GO, MeSH, and NCIT terms to Reactome and WikiPathways.</p>

<h2 id="species-specific-variant-of-a-pathway">Species-Specific Variant of a Pathway</h2>

<p>GO, MeSH, NCIT, and many other nomenclatures do not contain species-specific
variants of their pathways. However, KEGG contains both a parent pathway,
prefixed with <code class="language-plaintext highlighter-rouge">map</code> and species-specific pathway, prefixed with their internal 3
or 4-letter species code.</p>

<table>
  <thead>
    <tr>
      <th>Subject</th>
      <th>Predicate</th>
      <th>Object</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://identifiers.org/kegg.pathway:hsa04210">kegg.pathway:hsa04210</a></td>
      <td>speciesSpecific</td>
      <td><a href="https://identifiers.org/kegg.pathway:map04210">kegg.pathway:map04210</a></td>
    </tr>
    <tr>
      <td><a href="https://identifiers.org/kegg.pathway:bta04210">kegg.pathway:bta04210</a></td>
      <td>speciesSpecific</td>
      <td><a href="https://identifiers.org/kegg.pathway:map04210">kegg.pathway:map04210</a></td>
    </tr>
    <tr>
      <td>…</td>
      <td>…</td>
      <td>…</td>
    </tr>
  </tbody>
</table>

<p>It should generally hold that when <code class="language-plaintext highlighter-rouge">X speciesSpecific Y</code> and
<code class="language-plaintext highlighter-rouge">Y skos:exactMatch Z</code> are true, <code class="language-plaintext highlighter-rouge">X speciesSpecific Z</code>. This allows KEGG to serve
as a bridge between the species-specific and non-species-specific pathway
worlds. However, Domingo-Fernandez <em>et al.</em> showed that there are huge
discrepancies between KEGG, Reactome, and WikiPathways, so there is still need
to curate/infer the same kinds relationships in Reactome and WikiPathways.</p>

<p>Unfortunately, Reactome and WikiPathways do not (yet) have parent terms for
non-species-specific pathways. Asking about this was the point of the tweet that
inspired this blog post. Because Reactome uses a standardized nomenclature where
all variants of each pathway across species have the same numerical part to
their identifier (e.g.,
<a href="https://identifiers.org/reactome:R-HSA-109581">R-HSA-109581</a> and
<a href="https://identifiers.org/reactome:R-BTA-109581">R-BTA-109581</a>), they could
institute a similar parent nomenclature like KEGG’s. WikiPathways identifiers do
not have this sort of regularity, but they have the benefit of being highly
receptive to external input and improvements.</p>

<p>Side bar: I’ve seen an elegant solution for this in OBO that defines child terms
with an intersection of the
<a href="https://github.com/oborel/obo-relations">Relation Ontology</a> relation
<a href="https://identifiers.org/RO:0002160">RO:0002160</a> (only in taxon) to a given
species and the parent term, but this is an unnecessarily complicated
alternative for the goal of representing the relation between two entities.</p>

<h2 id="pathways-are-orthologs">Pathways are Orthologs</h2>

<p>Two genes with similar evolutionary history and function appearing in two
organisms are called orthologs. Orthology is incredibly important for studying
biology because it allows us to make inferences about how human biology works by
studying model organisms like mice and rats. There are several databases
collecting orthology relationships, such as
<a href="https://www.ncbi.nlm.nih.gov/homologene">HomoloGene</a>.</p>

<p>It follows that orthology could be applied to pathways as well. In fact,
Reactome’s web interface already has a box below each pathway linking to the
orthologous pathways as seen on
https://reactome.org/content/detail/R-HSA-109581:</p>

<p><img src="/img/reactome_orthology_box.png" alt="Reactome Orthology Box" /></p>

<p>However, this information is not programatically available (AFAIK), and it is
not available for other databases like WikiPathways and KEGG. Therefore, we can
introduce a relationship <code class="language-plaintext highlighter-rouge">orthology</code> to start curating triples like:</p>

<table>
  <thead>
    <tr>
      <th>Subject</th>
      <th>Predicate</th>
      <th>Object</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://identifiers.org/kegg.pathway:hsa04210">kegg.pathway:hsa04210</a></td>
      <td>orthology</td>
      <td><a href="https://identifiers.org/kegg.pathway:bta04210">kegg.pathway:bta04210</a></td>
    </tr>
  </tbody>
</table>

<p>Orthology relationships effectively convey the same information as
<code class="language-plaintext highlighter-rouge">speciesSpecific</code> with the advantage that they do not require the addition of a
parent term. However, between N orthologous pathways, there will be a complete
subggraph of (1/2) _ N _ (N-1) edges (also called a clique in graph theory).
Depending on the downstream use case, these kinds of subgraphs can be
problematic.</p>

<p>Because <code class="language-plaintext highlighter-rouge">kegg.pathway:hsa04210 skos:exactMatch reactome:R-HSA-109581</code>, we can
infer <code class="language-plaintext highlighter-rouge">reactome:R-HSA-109581 orthology kegg.pathway:bta04210</code>. However, I think
it would be best to only curate orthology relationships within a given database
because it will increase the size (N) of the clique.</p>

<h2 id="pathway-is-about-a-concept">Pathway is About a Concept</h2>

<p>KEGG, Reactome, and WikiPathways not only include pathways, but also other
“maps” about specific topics such as diseases, families of proteins, and other
biological entities.</p>

<p>For example, KEGG has an entry
<a href="https://identifiers.org/kegg.pathway:hsa05010">kegg.pathway:hsa05010</a> entitled
<em>Alzheimer disease - Homo sapiens (human)</em>. When using
<a href="https://github.com/indralab/gilda">Gilda</a> to generate lexical matchings, the
MeSH entry <a href="https://identifiers.org/mesh:D000544">mesh:D000544</a> (Alzheimer
Disease) appeared highly ranked. However, KEGG’s notion of pathway and MeSH’s
notion of a disease are not the same, and these two terms should not be
considered equivalent. For this case, not only KEGG but also Reactome and
WikiPathways, we can introduce a new relationship <code class="language-plaintext highlighter-rouge">pathwayAbout</code>. It turns out
that WikiPathways also has an Alzheimer’s disease “pathway” as well.</p>

<table>
  <thead>
    <tr>
      <th>Subject</th>
      <th>Predicate</th>
      <th>Object</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://identifiers.org/kegg.pathway:hsa05010">kegg.pathway:hsa05010</a></td>
      <td>pathwayAbout</td>
      <td><a href="https://identifiers.org/mesh:D000544">mesh:D000544</a></td>
    </tr>
    <tr>
      <td><a href="https://identifiers.org/wikipathways:WP2059">wikipathways:WP2059</a></td>
      <td>pathwayAbout</td>
      <td><a href="https://identifiers.org/mesh:D000544">mesh:D000544</a></td>
    </tr>
  </tbody>
</table>

<p>Note that KEGG and WikiPathways both have specificity in their pathways for
organisms, but diseases in MeSH and other nomenclatures aren’t typically
stratified by their target organisms. Therefore, the mouse-specific Alzheimer’s
disease pathway in WikiPathways (<code class="language-plaintext highlighter-rouge">wikipathways:WP2075</code>) could also have the same
relationship.</p>

<p>Another example is opsins - a family of light-sensitive proteins. Reactome has a
pathway <a href="https://identifiers.org/reactome:R-HSA-419771">reactome:R-HSA-419771</a>
(Opsins) that is not the same as the MeSH entry
<a href="https://identifiers.org/mesh:D055355">mesh:D055355</a> (Opsins) describing the
protein family.</p>

<p>There is specific interest in connecting disease maps appearing in pathway
databases to the diseases themselves. WikiPathways has already begun doing this
as can be seen on https://www.wikipathways.org/index.php/Pathway:WP2059.</p>

<p><img src="/img/wikipathways_ontology_terms.png" alt="WikiPathways Ontology Terms" /></p>

<p>It might be justified to propose an alternate relationship with more specific
semantics. More information on various disease-specific curation projects
outside major pathway databases can be found at https://disease-maps.org.</p>

<h2 id="disease-specific-variant-of-a-pathway">Disease-specific Variant of a Pathway</h2>

<p>This is a bit of an afterthought, but it might be mentioning that there are
places, like <a href="https://neurommsig.scai.fraunhofer.de/">NeuroMMSig</a>, that curate
disease-specific variants of pathways. These would need their own dedicated
relationships to connect to the “canonical” pathway and to the disease that they
describe.</p>

<hr />

<p>I have to give a huge shout-out to
<a href="https://github.com/ddomingof">Daniel Domingo-Fernández</a>,
<a href="https://github.com/jmarinllao">Josep Marín-Llaó</a>, Carlos Bobis-Álvarez, and
<a href="https://github.com/YojanaGadiya">Yojana Gadiya</a> who have done the curation in
the ComPath project as well as <a href="https://github.com/bgyori/">Ben Gyori</a> who laid
the groundwork for improving the lexical mappings with the Gilda software as
well as contributed tons of curations for MeSH-GO mappings.</p>

<p>There are still many disjoint resources that need normalization, including the
Pathway Ontology, which looks to have lots of information. I’ll be working on it
via this <a href="https://github.com/biopragmatics/biomappings/issues/36">GitHub issue</a>.</p>

<p>There’s also PathBank, (curated by Yojana but not appearing in the original
ComPath publication), BioCyc, MetaCyc, and many others. Each must first be
included in <a href="https://github.com/pyobo/pyobo">PyOBO</a> as I described in a
<a href="/2020/04/18/ooh-na-na.html">previous post</a> before getting into
curation, so I can reuse all the code.</p>

<p>I’m not super happy with any of the names I’ve given to relationships in this
post, either. I’m open to suggestion for improvement. We alternatively discussed
using <code class="language-plaintext highlighter-rouge">skos:broader</code> and <code class="language-plaintext highlighter-rouge">skos:narrower</code>, as well. Further, I’d love to see
these kinds of relationships appear in the Relation Ontology itself, but
unfortunately I have not been super successful in petitioning them for
improvements in the past, so I may start another open ontology project focused
on relationships themselves.</p>

<p>This is all part of a greater effort,
<a href="https://github.com/biopragmatics/biomappings">Biomappings</a>, which Ben and I
have been working on to make it much easier to curate equivalences and related
mappings. I’ll have more to say about that in a future post.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="ontology" /><category term="curation" /><summary type="html"><![CDATA[Domingo-Fernandez et al. published ComPath: An ecosystem for exploring, analyzing, and curating mappings across pathway databases. in 2018 describing the overlap between human pathways in KEGG, Reactome, and WikiPathways. A lot of the underlying machinery I developed to support this project has been improved since, and it’s time to spread the search to other organisms besides humans and other databases. This blog post is about some additional relation types needed to capture the relations between pathways appearing in these databases.]]></summary></entry><entry><title type="html">Making DrugBank Reproducible</title><link href="https://cthoyt.com/2020/12/14/taming-drugbank.html" rel="alternate" type="text/html" title="Making DrugBank Reproducible" /><published>2020-12-14T08:00:00+00:00</published><updated>2020-12-14T08:00:00+00:00</updated><id>https://cthoyt.com/2020/12/14/taming-drugbank</id><content type="html" xml:base="https://cthoyt.com/2020/12/14/taming-drugbank.html"><![CDATA[<p>If you’re reading my blog, there’s a pretty high chance you’ve used
<a href="https://go.drugbank.com/">DrugBank</a>, a database of drug-target interactions,
drug-drug interactions, and other high-granularity information about
clinically-studied chemicals. DrugBank has two major problems, though: its data
are password-protected, and its license does not allow redistribution. Time to
solve these problems once and for all.</p>

<p>I’d guess that most of the thousands of people who use DrugBank have run into
the same issue as me to get the data: you have to navigate through the DrugBank
site, create an account, log in, then download the data manually. There are some
hints on the site on how this can be done through the shell, but I think it’s
the unfortunate case that many people using bioinformatics resources just aren’t
comfortable using the shell. Further, the file is zipped, which means that
unzipping it requires further knowledge of the arcane arts of which shell
programs and which flags to use with them.</p>

<p>Even after getting past this, users need to write their own programs that read
the XML content of the file, extract the relevant parts, and put it somewhere
for later. If you’re like me, you probably want to have as little to do with
data in the XML format as possible, and you probably deleted the original zip
archive along with the XML file after you got the data out of it that you
<em>actually</em> wanted.</p>

<p>The trouble with the scenario described in the last two paragraphs is that any
analysis done on the resulting files required lots of manual steps in the
middle. These steps can’t be automated, and therefore the downstream analysis
can’t be automated either. It’s highly likely for a research to have the time or
motivation to do these steps exactly the same way as described in a paper, if
it’s even described at all.</p>

<p>Because of DrugBank’s licensing rules, you can’t redistribute the data in the
format that is necessary for downstream use either.</p>

<p>This is no small problem - hundreds of papers cite DrugBank per year. The hype
of drug repositioning for COVID-19 no doubt helped in 2020, with 147 citations
as of the time of writing this post on December 14th, 2020.</p>

<p><img src="/img/drugbank_citations.png" alt="DrugBank Citations" /></p>

<p>Search conducted on December 14th, 2020 with
https://pubmed.ncbi.nlm.nih.gov/?term=drugbank.</p>

<p>It’s difficult to tell which, if any, of these efforts are meaningful. Without
the ability to reproduce the steps taken to do analyses based on DrugBank, we
can not even begin to evaluate these papers. Even further - imagine the intense
pressure on the few famous scientists in network-based drug repositioning who
receive the most requests to review more papers. How could we possibly expect
these scientists to provide thorough reviews if it’s not easy (or possible, for
that matter) for them to re-produce the analyses?</p>

<h2 id="the-solution">The Solution</h2>

<p>The first time I put to writing the need to automate the acquisition of data was
in my (still not accepted through peer review)
<a href="https://www.biorxiv.org/content/10.1101/631812v1">article</a> on the reproducible
acquisition and conversion a wide range of biological databases into
<a href="https://biological-expression-language.github.io">Biological Expression Language</a>.
In this article, we presented a suite of 50+ scripts which each downloaded,
converted, and exported a biological database like DrugBank, GWAS Catalog,
HIPPIE, and others into a common schema for networks biology downstream
applications like signal transduction pathway analysis, knowledge graph
embedding, and other methods. Of all of them, DrugBank remained the most vexing.</p>

<p>It’s time for a solution - for code that can help get us out of this rut. This
code needs to be able to download the data from DrugBank, open it up, and help
transform it all without asking the user to intervene too much.</p>

<p>Enter <a href="https://github.com/cthoyt/drugbank_downloader"><code class="language-plaintext highlighter-rouge">drugbank_downloader</code></a>.
This is a Python library with three functions exactly for this. First, you have
to install it with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pip <span class="nb">install </span>drugbank-downloader
</code></pre></div></div>

<h3 id="download-a-specific-version">Download A Specific Version</h3>

<p>If you want your analysis to be reproducible, you should specify what version of
the database you want to use. Luckily, DrugBank does keep an
<a href="https://go.drugbank.com/releases">archive of old versions</a>. The following code
describes how to use the <code class="language-plaintext highlighter-rouge">drugbank_downloader.download_drugbank()</code> function,
which asks for a version, a DrugBank username, and the corresponding DrugBank
password. You’re free to get the username and password into the script however
you want - usually loading from the environment using <code class="language-plaintext highlighter-rouge">os.getenv()</code> is a good
choice. Hard-coding them is not so much, because it could potentially violate
the terms and conditions of use of DrugBank, and just isn’t good practice in
general, especially if you reuse passwords. Get
<a href="https://www.lastpass.com">LastPass</a>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">from</span> <span class="nn">drugbank_downloader</span> <span class="kn">import</span> <span class="n">download_drugbank</span>

<span class="n">username</span> <span class="o">=</span> <span class="p">...</span>  <span class="c1"># suggestion: load from environment with os.getenv('DRUGBANK_USERNAME')
</span><span class="n">password</span> <span class="o">=</span> <span class="p">...</span>

<span class="n">path</span> <span class="o">=</span> <span class="n">download_drugbank</span><span class="p">(</span><span class="n">version</span><span class="o">=</span><span class="s">'5.1.7'</span><span class="p">,</span> <span class="n">username</span><span class="o">=</span><span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="o">=</span><span class="n">password</span><span class="p">)</span>

<span class="c1"># This is where it gets downloaded: ~/.data/drugbank/5.1.7/full database.xml.zip
</span><span class="n">expected_path</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">expanduser</span><span class="p">(</span><span class="s">'~'</span><span class="p">),</span> <span class="s">'.data'</span><span class="p">,</span> <span class="s">'drugbank'</span><span class="p">,</span> <span class="s">'5.1.7'</span><span class="p">,</span> <span class="s">'full database.xml.zip'</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">expected_path</span> <span class="o">==</span> <span class="n">path</span><span class="p">.</span><span class="n">as_posix</span><span class="p">()</span>
</code></pre></div></div>

<p>This script takes care of downloading the data and storing it at
<code class="language-plaintext highlighter-rouge">~/.data/drugbank/5.1.7/full database.xml.zip</code>. After it’s been downloaded once,
the function is smart and doesn’t need to download again. This is all taken care
of by the <a href="https://github.com/cthoyt/pystow"><code class="language-plaintext highlighter-rouge">pystow</code></a> behind the scenes. If you
specify an alternate version, it’s pretty obvious where in the filepath it
changes.</p>

<h3 id="download-the-latest-version">Download the Latest Version</h3>

<p>If you want to automate running your code on the most recent version of
DrugBank, you can use the same function, but you’ll first have to install
<a href="https://github.com/biopragmatics/bioversions"><code class="language-plaintext highlighter-rouge">bioversions</code></a> with
<code class="language-plaintext highlighter-rouge">pip install bioversions</code>. This package’s job is to look up the most recent
version of varions biological databases. I’ll have a whole post on that later.
Then, you can modify the previous code slightly by simply removing the <code class="language-plaintext highlighter-rouge">version</code>
argument.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">from</span> <span class="nn">drugbank_downloader</span> <span class="kn">import</span> <span class="n">download_drugbank</span>

<span class="n">username</span> <span class="o">=</span> <span class="p">...</span>  <span class="c1"># suggestion: load from environment with os.getenv('DRUGBANK_USERNAME')
</span><span class="n">password</span> <span class="o">=</span> <span class="p">...</span>

<span class="n">path</span> <span class="o">=</span> <span class="n">download_drugbank</span><span class="p">(</span><span class="n">username</span><span class="o">=</span><span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="o">=</span><span class="n">password</span><span class="p">)</span>

<span class="c1"># This is where it gets downloaded: ~/.data/drugbank/5.1.7/full database.xml.zip based on the latest
# version, as of December 14th, 2020.
</span><span class="n">expected_path</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">expanduser</span><span class="p">(</span><span class="s">'~'</span><span class="p">),</span> <span class="s">'.data'</span><span class="p">,</span> <span class="s">'drugbank'</span><span class="p">,</span> <span class="s">'5.1.7'</span><span class="p">,</span> <span class="s">'full database.xml.zip'</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">expected_path</span> <span class="o">==</span> <span class="n">path</span><span class="p">.</span><span class="n">as_posix</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="dont-bother-unpacking---read-full-databasexmlzip-directly">Don’t Bother Unpacking - read <code class="language-plaintext highlighter-rouge">full database.xml.zip</code> Directly</h3>

<p>Like I said before, it’s not enough just to get the file. It needs to be
unzipped and opened. If you’re not familiar with doing this in python, you can
use the following code:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">zipfile</span>
<span class="kn">from</span> <span class="nn">drugbank_downloader</span> <span class="kn">import</span> <span class="n">download_drugbank</span>

<span class="n">path</span> <span class="o">=</span> <span class="n">download_drugbank</span><span class="p">(</span><span class="n">username</span><span class="o">=</span><span class="p">...,</span> <span class="n">password</span><span class="o">=</span><span class="p">...)</span>

<span class="k">with</span> <span class="n">zipfile</span><span class="p">.</span><span class="n">ZipFile</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="k">as</span> <span class="n">zip_file</span><span class="p">:</span>
    <span class="k">with</span> <span class="n">zip_file</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="s">'full database.xml'</span><span class="p">)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
        <span class="k">pass</span>  <span class="c1"># do something with the file
</span></code></pre></div></div>

<p>You don’t have time to remember this. Just use
<code class="language-plaintext highlighter-rouge">drugbank_downloader.open_drugbank()</code> instead:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">drugbank_downloader</span> <span class="kn">import</span> <span class="n">open_drugbank</span>

<span class="k">with</span> <span class="n">open_drugbank</span><span class="p">(</span><span class="n">username</span><span class="o">=</span><span class="p">...,</span> <span class="n">password</span><span class="o">=</span><span class="p">...)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
    <span class="k">pass</span>  <span class="c1"># do something with the file, same as above
</span></code></pre></div></div>

<p>There’s some magic using <code class="language-plaintext highlighter-rouge">contextlib</code> to make this work with the <code class="language-plaintext highlighter-rouge">with</code>
statement. Just keep reading, though…</p>

<h3 id="reading-drugbanks-xml">Reading Drugbank’s XML</h3>

<p>After you’ve opened the file, you probably want to read it with an XML parser
like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">xml.etree</span> <span class="kn">import</span> <span class="n">ElementTree</span>
<span class="kn">from</span> <span class="nn">drugbank_downloader</span> <span class="kn">import</span> <span class="n">open_drugbank</span>

<span class="k">with</span> <span class="n">open_drugbank</span><span class="p">(</span><span class="n">username</span><span class="o">=</span><span class="p">...,</span> <span class="n">password</span><span class="o">=</span><span class="p">...)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
    <span class="n">tree</span> <span class="o">=</span> <span class="n">ElementTree</span><span class="p">.</span><span class="n">parse</span><span class="p">(</span><span class="nb">file</span><span class="p">)</span>
<span class="n">root</span> <span class="o">=</span> <span class="n">tree</span><span class="p">.</span><span class="n">getroot</span><span class="p">()</span>
</code></pre></div></div>

<p>You don’t have time to remember this either. Just use
<code class="language-plaintext highlighter-rouge">drugbank_downloader.parse_drugbank()</code> instead:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">drugbank_downloader</span> <span class="kn">import</span> <span class="n">parse_drugbank</span>

<span class="n">root</span> <span class="o">=</span> <span class="n">parse_drugbank</span><span class="p">(</span><span class="n">username</span><span class="o">=</span><span class="p">...,</span> <span class="n">password</span><span class="o">=</span><span class="p">...)</span>
</code></pre></div></div>

<h2 id="drugbank-endgame">DrugBank: Endgame</h2>

<p>Once you’ve got the <code class="language-plaintext highlighter-rouge">XML</code> python object, you can write all of the code to
extract the parts you want, load them in a <code class="language-plaintext highlighter-rouge">pandas.DataFrame</code>, or do whatever
you’d like. If these steps are slow, you could even write code that caches it in
the middle. Below is my template for an expensive processing step.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kn">from</span> <span class="nn">drugbank_downloader</span> <span class="kn">import</span> <span class="n">parse_drugbank</span>

<span class="k">def</span> <span class="nf">process_data</span><span class="p">():</span>
    <span class="p">...</span>

<span class="k">def</span> <span class="nf">save_my_parsing_results</span><span class="p">(</span><span class="n">processed_data</span><span class="p">,</span> <span class="nb">file</span><span class="p">):</span>
    <span class="p">...</span>

<span class="k">def</span> <span class="nf">load_my_parsing_results</span><span class="p">(</span><span class="n">path</span><span class="p">):</span>
    <span class="p">...</span>

<span class="k">def</span> <span class="nf">get_processed_data</span><span class="p">():</span>
    <span class="n">cache_path</span> <span class="o">=</span> <span class="p">...</span>
    <span class="k">if</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">exists</span><span class="p">(</span><span class="n">cache_path</span><span class="p">):</span>
        <span class="n">processed_data</span> <span class="o">=</span> <span class="n">load_my_parsing_results</span><span class="p">(</span><span class="n">cache_path</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="n">root</span> <span class="o">=</span> <span class="n">parse_drugbank</span><span class="p">(</span><span class="n">username</span><span class="o">=</span><span class="p">...,</span> <span class="n">password</span><span class="o">=</span><span class="p">...)</span>
        <span class="n">processed_data</span> <span class="o">=</span> <span class="n">process_data</span><span class="p">(</span><span class="n">root</span><span class="p">)</span>
        <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">cache_path</span><span class="p">,</span> <span class="s">'w'</span><span class="p">)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
            <span class="n">save_my_parsing_results</span><span class="p">(</span><span class="n">root</span><span class="p">,</span> <span class="nb">file</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">processed_data</span>
</code></pre></div></div>

<hr />

<p>I understand that most scientists aren’t trained as software engineers, nor are
they incentivized to write code that’s reproducible, but I at least hope this
code helps cut a few non-reproducible steps out of your science. Happy drug
hunting!</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="reproducibility" /><summary type="html"><![CDATA[If you’re reading my blog, there’s a pretty high chance you’ve used DrugBank, a database of drug-target interactions, drug-drug interactions, and other high-granularity information about clinically-studied chemicals. DrugBank has two major problems, though: its data are password-protected, and its license does not allow redistribution. Time to solve these problems once and for all.]]></summary></entry><entry><title type="html">Scoring Inverse Triples</title><link href="https://cthoyt.com/2020/12/11/inverse-triple-scoring.html" rel="alternate" type="text/html" title="Scoring Inverse Triples" /><published>2020-12-11T08:00:00+00:00</published><updated>2020-12-11T08:00:00+00:00</updated><id>https://cthoyt.com/2020/12/11/inverse-triple-scoring</id><content type="html" xml:base="https://cthoyt.com/2020/12/11/inverse-triple-scoring.html"><![CDATA[<p>When training a knowledge graph embedding model with inverse triples, two scores
are learned for every triple <code class="language-plaintext highlighter-rouge">(h, r, t)</code> - one for the original and one for the
inverse triple <code class="language-plaintext highlighter-rouge">(t, r', h)</code>. This blog post is about investigating when/why
there might be meaningful differences between those scores depending on the
dataset, model, and training assumption.</p>

<p>In <a href="https://github.com/pykeen/pykeen/">PyKEEN</a>, each interaction model (e.g.,
TransE, ConvE, Tucker) inherits from the base model <code class="language-plaintext highlighter-rouge">pykeen.models.Model</code> and
implements the scoring methods <code class="language-plaintext highlighter-rouge">Model.score_h()</code>, <code class="language-plaintext highlighter-rouge">Model.score_r()</code>,
<code class="language-plaintext highlighter-rouge">Model.score_t()</code>, and <code class="language-plaintext highlighter-rouge">Model.score_hrt()</code>. The last, <code class="language-plaintext highlighter-rouge">score_hrt()</code> takes in a
triple or sequence of triples and calculates a real-valued score for each. This
is quite efficient in batch because of the way most models are implemented with
matrix multiplication in PyTorch. The remaining three take pairs:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">score_h()</code> takes relation/tail pairs and provides scores across all possible
tails</li>
  <li><code class="language-plaintext highlighter-rouge">score_r()</code> takes head/tail pairs and provides scores across all possible
relations</li>
  <li><code class="language-plaintext highlighter-rouge">score_t()</code> takes head/relation pair and provides scores across all possible
heads</li>
</ul>

<p>Each of these functions are used slightly differently during the training and
inference workflows. One of the reviewers for the
<a href="https://arxiv.org/abs/2007.14175">PyKEEN software paper</a> pointed out that the
while <code class="language-plaintext highlighter-rouge">Model.predict_scores_all_tails()</code> and <code class="language-plaintext highlighter-rouge">Model.predict_scores_all_heads()</code>
inference workflows allow the user to choose if the forward triples or inverse
triples are used for predictions, that this functionality is not exposed to the
user for use in further study.</p>

<p>Mehdi (<a href="https://github.com/mali-git">@mali-git</a>; he’s a really good guy) began
<a href="https://github.com/pykeen/pykeen/pull/208">pykeen/pykeen#208</a> refactoring the
implementation of the head prediction and tail prediction workflows in the
<code class="language-plaintext highlighter-rouge">pykeen.model.Model</code> base model class that better abstracted the operations for
generating inverse triples, and how they are applied in each inference workflow.</p>

<p>I often serve the role of project manager for PyKEEN, so I was initially a bit
skeptical of adding new functionality without some good examples of what a user
might do with it. After a bit of discussion, Mehdi suggested we could showcase a
comparison the distributions of forward and inverse triples scores and provided
a minimal example. While he was busy <em>actually</em> working on the implementation, I
took his example to the extreme and created an entire experimental setup for
this investigation to accompany the new implementation.</p>

<h2 id="comparison-of-distributions">Comparison of Distributions</h2>

<p>Medhi provided a new implementation of <code class="language-plaintext highlighter-rouge">Model.score_hrt_inverse</code> for exactly
this kind of investigation. The following code can be used to get the scores for
the testing triples from the Nations dataset.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pykeen.datasets</span> <span class="kn">import</span> <span class="n">Nations</span>
<span class="kn">from</span> <span class="nn">pykeen.pipeline</span> <span class="kn">import</span> <span class="n">pipeline</span>

<span class="n">nations</span> <span class="o">=</span> <span class="n">Nations</span><span class="p">()</span>
<span class="n">testing_triples</span> <span class="o">=</span> <span class="n">nations</span><span class="p">.</span><span class="n">testing</span><span class="p">.</span><span class="n">mapped_triples</span>

<span class="n">res</span> <span class="o">=</span> <span class="n">pipeline</span><span class="p">(</span>
    <span class="n">dataset</span><span class="o">=</span><span class="n">nations</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">'RotatE'</span><span class="p">,</span>
    <span class="n">training_loop</span><span class="o">=</span><span class="s">'LCWA'</span><span class="p">,</span>
    <span class="n">training_kwargs</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="n">num_epochs</span><span class="o">=</span><span class="mi">60</span><span class="p">),</span>
<span class="p">)</span>

<span class="c1"># Score with original triples
</span><span class="n">scores_forward</span> <span class="o">=</span> <span class="n">res</span><span class="p">.</span><span class="n">model</span><span class="p">.</span><span class="n">score_hrt</span><span class="p">(</span><span class="n">testing_triples</span><span class="p">)</span>

<span class="c1"># Score with inverse triples
</span><span class="n">scores_inverse</span> <span class="o">=</span> <span class="n">res</span><span class="p">.</span><span class="n">model</span><span class="p">.</span><span class="n">score_hrt_inverse</span><span class="p">(</span><span class="n">testing_triples</span><span class="p">)</span>
</code></pre></div></div>

<p>These results are both PyTorch tensors, so don’t forget to use
<code class="language-plaintext highlighter-rouge">scores_*.detach().numpy()</code> for use in your own comparisons and visualizations.
The first thing I did was plotted the distributions of both with
<a href="https://seaborn.pydata.org/generated/seaborn.histplot.html"><code class="language-plaintext highlighter-rouge">seaborn.histplot</code></a>.</p>

<p>The following two plots show that sometimes the distributions match up pretty
well, and sometimes they don’t. The distributions are shown normalized to make
it easier to compare - the shapes are more important than the actual counts
because the number of testing triples changes from dataset to dataset. Sorry
about the inconsistency in bin widths - I’ve tried a lot and haven’t figured it
out. If you know the solution, please let me know (or make a PR to PyKEEN for
ultimate PR cred).</p>

<table>
  <thead>
    <tr>
      <th>Nations</th>
      <th>Kinships</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><img src="/img/inverse_triple_scoring/nations_rotate_lcwa_overlay.png" alt="Comparison of Distributions for Nations/RotatE/LCWA" /></td>
      <td><img src="/img/inverse_triple_scoring/kinships_rotate_lcwa_overlay.png" alt="Comparison of Distributions for Kinships/RotatE/LCWA" /></td>
    </tr>
  </tbody>
</table>

<p>This was just one comparison for the same model/training assumption. In order to
make it possible to look at many facets at the same time, the data need to be
simplified. Because the scores have a pairwise correspondence (the same triple
was scored two ways), it makes sense to plot distribution of residuals (the
forward score minus the inverse score). Here’s the same plots simplified with
residuals:</p>

<table>
  <thead>
    <tr>
      <th>Nations</th>
      <th>Kinships</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><img src="/img/inverse_triple_scoring/nations_rotate_lcwa_residuals.png" alt="Comparison of Residuals for Nations/RotatE/LCWA" /></td>
      <td><img src="/img/inverse_triple_scoring/kinships_rotate_lcwa_residuals.png" alt="Comparison of Residuals for Kinships/RotatE/LCWA" /></td>
    </tr>
  </tbody>
</table>

<p>The PyKEEN core team is a very special place - Max, Mehdi, and Laurent are all
excellent machine learning researchers and scientists. The part where I excel is
in organization and software development, so I’ve taken care of aspects like the
<code class="language-plaintext highlighter-rouge">pipeline()</code>, visualization, and data science aspects of our work. Naturally, I
gravitate towards any tasks that can be solved with super deep loops. So without
further ado, below are presented the results from the script that runs this
experiment for all datasets, models, and training assumptions. It assigns row by
dataset, column by model, and color by training assumption. Note: the score axes
will not be share because each model produces different ranges of scores with
different density functions. This itself is also an interesting and upcoming
avenue of research!</p>

<p><img src="/img/inverse_triple_scoring/inverse_scores_residuals.png" alt="Inverse Scores Residuals" /></p>

<p>While the
<a href="https://github.com/pykeen/pykeen/blob/predict_with_inverse/src/pykeen/experiments/inverse_stability.py">script</a>
that generated this chart is extensible to all datasets and models, I haven’t
got the computational power available today to run them all. All that needs to
be done is to add a few more strings to the lists of models, or even better,
replace them with the
<a href="https://github.com/pykeen/pykeen/issues/114">python entrypoint-loaded classes</a>.
However, with many experiments, I think the small datasets like Nations and
Kinships are sufficient to demonstrate a given phenomena.</p>

<p>There are a few major insights to be taken from this chart. First, the RotatE,
TransE, and DistMult model seem to operate pretty similarly for forwards and
inverse triples. This is demonstrated by two aspects of the distributions -
their normality and tightness (low standard deviation). When considering that
each of these are translational distance models, this should come as no
surprise.</p>

<p>ComplEx shows neither normality in its distributions nor tightness. This is
because ComplEx’s interaction function is explicitly supposed to model the
directionality of triples through its use of complex-valued entity embeddings
and the use of the non-commutative multiplication operation on complex numbers.</p>

<p>SimplE is an interesting model to consider when studying the difference between
forward and inverse triples because it explicitly models the forward and inverse
of a given triple then takes the average for its final score. Its strange
behavior based on the difference between the stochastic local closed-world
assumption (sLCWA) and local closed-word assumption (LCWA) during training could
be due to the fact that it is a complete graph (e.g, all pairs of entities have
a triple in both directions).</p>

<p>There are definitely more insights and more satisfying explanations to be gained
by looking at a larger-scale analysis, but this shows that very quickly, with
the artifacts of training and very little time, insight can be gleamed into the
possible drawbacks for inference for a given dataset-model-training assumption
combination.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="knowledge graphs" /><category term="pykeen" /><summary type="html"><![CDATA[When training a knowledge graph embedding model with inverse triples, two scores are learned for every triple (h, r, t) - one for the original and one for the inverse triple (t, r', h). This blog post is about investigating when/why there might be meaningful differences between those scores depending on the dataset, model, and training assumption.]]></summary></entry><entry><title type="html">Generating Testing Knowledge Graphs with Literals</title><link href="https://cthoyt.com/2020/12/07/generating-literal-datasets.html" rel="alternate" type="text/html" title="Generating Testing Knowledge Graphs with Literals" /><published>2020-12-07T08:00:00+00:00</published><updated>2020-12-07T08:00:00+00:00</updated><id>https://cthoyt.com/2020/12/07/generating-literal-datasets</id><content type="html" xml:base="https://cthoyt.com/2020/12/07/generating-literal-datasets.html"><![CDATA[<p><a href="https://github.com/pykeen/pykeen/">PyKEEN</a> has a wide variety of functionality
related to knowledge graph embedding models and handling various sources of
knowledge graphs. This post describes the journey towards properly testing the
functionality of an exotic set of knowledge graph embedding models that
incorporate feature vectors for entities via triples with numeric literals.</p>

<p>PyKEEN implements the <a href="https://github.com/SmartDataAnalytics/LiteralE">LiteralE</a>
class of knowledge graph embedding models that allow for the inclusion of
numeric features for each entity. It then combines these features with the
embedding for each entity using the <code class="language-plaintext highlighter-rouge">g</code> function before calculating a score
using an arbitrary interaction function. In the
<a href="https://arxiv.org/abs/1802.00934">original paper</a>, Kristiadi <em>et al.</em> used the
interaction functions from the DistMult and ComplEx.</p>

<p>Unfortunately, the original paper used three datasets for benchmarking:
FB15k-237, WN18RR, and YAGO3-10. While these aren’t the largest datasets PyKEEN
can handle, they are too big to be used during unit testing, which should run
very quickly and be as minimal as possible. Therefore, we punted writing proper
tests for LiteralE models because of the lack of availability of a small,
well-studied dataset like Nations or Kinships.</p>

<p>Today, that came to bite us in the butt because we decided to improve the
implementation of the underlying interface for loading triples from files (see
<a href="https://github.com/pykeen/pykeen/pull/193">pykeen/pykeen#193</a>). That meant that
it was time to set off on creating a dataset that was small and had literals in
it and implementing proper tests for the LiteralE models.</p>

<p>My first idea was to take an arbitrary dataset, like Nations, and generate
random features for it. I implemented an algorithm that created (slightly
meaningful) random representations, but then <a href="https://github.com/mberr/">@mberr</a>
gave the excellent suggestion that the CIA’s
<a href="https://www.cia.gov/library/publications/the-world-factbook/">World FactBook</a>
contains all sorts of information for each country in tabular form, and that I
could use that to extend the Nations dataset to have literals. I did the dirty
work of grabbing some features (area, population, etc.) for the ten entities in
the Nations dataset then a bit of improvement on the underlying PyKEEN code for
dataset loading. It all come to fruition in
<a href="https://github.com/pykeen/pykeen/pull/199">pykeen/pykeen#199</a> where @mberr
helped update the LiteralE implementations, but he rightfully pointed out that
my random dataset generation code was no longer necessary and didn’t belong in
the pull request.</p>

<p>Luckily, I have a blog, which seems like a great place to share my code and my
thought process!</p>

<h2 id="generating-random-literals">Generating Random Literals</h2>

<p>The algorithm was the following:</p>

<ol>
  <li>Pick a pre-existing dataset</li>
  <li>Train a knowledge graph embedding model on the dataset</li>
  <li>Generate a random transformation matrix that can be multiplied by each
entity’s embedding to generate a feature</li>
  <li>Apply a bit of noise</li>
  <li>Assign each feature a random name and write to a file whose rows are in the
form of <entity> <random feature="" name=""> <random feature="" value=""></random></random></entity></li>
</ol>

<p>While it would have been possible to directly generate random values for each
dummy feature, using a transformation on the entity embeddings means that each
feature will have some correlation to the embeddings itself, which could be
useful for testing later.</p>

<p>I’ve posted the code to Gist in case we ever need it again. Please feel free to
reuse, this code is under the MIT License.</p>

<script src="https://gist.github.com/cthoyt/fc0032168607b5b8bad342393fed0773.js"></script>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="knowledge graphs" /><category term="pykeen" /><summary type="html"><![CDATA[PyKEEN has a wide variety of functionality related to knowledge graph embedding models and handling various sources of knowledge graphs. This post describes the journey towards properly testing the functionality of an exotic set of knowledge graph embedding models that incorporate feature vectors for entities via triples with numeric literals.]]></summary></entry><entry><title type="html">Referring to SARS-CoV-2 Proteins in BEL</title><link href="https://cthoyt.com/2020/09/17/sars-cov-2-proteins-in-bel.html" rel="alternate" type="text/html" title="Referring to SARS-CoV-2 Proteins in BEL" /><published>2020-09-17T00:05:00+00:00</published><updated>2020-09-17T00:05:00+00:00</updated><id>https://cthoyt.com/2020/09/17/sars-cov-2-proteins-in-bel</id><content type="html" xml:base="https://cthoyt.com/2020/09/17/sars-cov-2-proteins-in-bel.html"><![CDATA[<p>Many of the proteins in the severe acute respiratory syndrome coronavirus 2
(SARS-CoV-2) are cleavage products of the replicase polyprotein 1ab
(<a href="https://identifiers.org/uniprot:P0DTD1">uniprot:P0DTD1</a>). Unfortunately, the
bioinformatics community is not so comfortable with proteins like this and
nomenclature remains tricky. Luckily, the
<a href="https://biological-expression-language.github.io">Biological Expression Language (BEL)</a>
has exactly the right tool to encode information about these proteins using the
<code class="language-plaintext highlighter-rouge">fragment()</code> function.</p>

<p><img src="/img/sars-cov-2-genome.jpeg" alt="SARS-CoV-2 Genome" /></p>

<p>This image was modified from the C&amp;EN article
<a href="https://cen.acs.org/biological-chemistry/infectious-disease/know-novel-coronaviruss-29-proteins/98/web/2020/04">What do we know about the novel coronavirus’s 29 proteins?</a></p>

<p>UniProt lists each of the 16 non-structural proteins (often written as symbols
nsp1-nsp16) as protein chains of the main protein entry, uniprot:P0DTD1. These
chains are assigned identifiers following the regular expression pattern of
<code class="language-plaintext highlighter-rouge">PRO_\d{10}</code>. The Identifiers.org registered this pattern under the prefix
<a href="https://registry.identifiers.org/registry/uniprot.chain"><code class="language-plaintext highlighter-rouge">uniprot.chain</code></a>.
While it resolves to URLs following the pattern of
<code class="language-plaintext highlighter-rouge">https://www.uniprot.org/uniprot/&lt;uniprot_id&gt;#&lt;chain_id&gt;</code>, it appears that the
parent protein’s UniProt identifier is looked up automatically . This is really
good news and means that we can start using stable CURIEs to identify these
proteins, even if like me, you’ve never used this prefix before.</p>

<p>Alternatively, BEL allows you to write out the relationship between the parent
protein and the fragment using the <code class="language-plaintext highlighter-rouge">fragment() / frag()</code> function
(<a href="https://biological-expression-language.github.io/entities/physical/#protein-fragments">docs</a>).
For example, the nsp1 fragment from position 1-180 can be written in BEL either
as <code class="language-plaintext highlighter-rouge">p(uniprot.chain:PRO_0000449619)</code> or as a fragment
<code class="language-plaintext highlighter-rouge">p(uniprot:P0DTD1 ! R1AB_SARS2, frag(1_180))</code>. The entire table of
non-structural proteins is written out below for your copy/paste convenience in
BEL coding.</p>

<table>
  <thead>
    <tr>
      <th>Symbol</th>
      <th>Chain</th>
      <th>Positions</th>
      <th>Name</th>
      <th>BEL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>nsp1</td>
      <td>PRO_0000449619</td>
      <td>1 – 180</td>
      <td>Host translation inhibitor nsp1</td>
      <td><code class="language-plaintext highlighter-rouge">p(uniprot:P0DTD1 ! R1AB_SARS2, frag(1_180))</code></td>
    </tr>
    <tr>
      <td>nsp2</td>
      <td>PRO_0000449620</td>
      <td>181 – 818</td>
      <td>Non-structural protein 2</td>
      <td><code class="language-plaintext highlighter-rouge">p(uniprot:P0DTD1 ! R1AB_SARS2, frag(181_818))</code></td>
    </tr>
    <tr>
      <td>nsp3</td>
      <td>PRO_0000449621</td>
      <td>819 – 2763</td>
      <td>Non-structural protein 3</td>
      <td><code class="language-plaintext highlighter-rouge">p(uniprot:P0DTD1 ! R1AB_SARS2, frag(819_2763))</code></td>
    </tr>
    <tr>
      <td>nsp4</td>
      <td>PRO_0000449622</td>
      <td>2764 – 3263</td>
      <td>Non-structural protein 4</td>
      <td><code class="language-plaintext highlighter-rouge">p(uniprot:P0DTD1 ! R1AB_SARS2, frag(2764_3263))</code></td>
    </tr>
    <tr>
      <td>nsp5</td>
      <td>PRO_0000449623</td>
      <td>3264 – 3569</td>
      <td>3C-like proteinase</td>
      <td><code class="language-plaintext highlighter-rouge">p(uniprot:P0DTD1 ! R1AB_SARS2, frag(3264_3569))</code></td>
    </tr>
    <tr>
      <td>nsp6</td>
      <td>PRO_0000449624</td>
      <td>3570 – 3859</td>
      <td>Non-structural protein 6</td>
      <td><code class="language-plaintext highlighter-rouge">p(uniprot:P0DTD1 ! R1AB_SARS2, frag(3570_3859))</code></td>
    </tr>
    <tr>
      <td>nsp7</td>
      <td>PRO_0000449625</td>
      <td>3860 – 3942</td>
      <td>Non-structural protein 7</td>
      <td><code class="language-plaintext highlighter-rouge">p(uniprot:P0DTD1 ! R1AB_SARS2, frag(3860_3942))</code></td>
    </tr>
    <tr>
      <td>nsp8</td>
      <td>PRO_0000449626</td>
      <td>3943 – 4140</td>
      <td>Non-structural protein 8</td>
      <td><code class="language-plaintext highlighter-rouge">p(uniprot:P0DTD1 ! R1AB_SARS2, frag(3943_4140))</code></td>
    </tr>
    <tr>
      <td>nsp9</td>
      <td>PRO_0000449627</td>
      <td>4141 – 4253</td>
      <td>Non-structural protein 9</td>
      <td><code class="language-plaintext highlighter-rouge">p(uniprot:P0DTD1 ! R1AB_SARS2, frag(4141_4253))</code></td>
    </tr>
    <tr>
      <td>nsp10</td>
      <td>PRO_0000449628</td>
      <td>4254 – 4392</td>
      <td>Non-structural protein 10</td>
      <td><code class="language-plaintext highlighter-rouge">p(uniprot:P0DTD1 ! R1AB_SARS2, frag(4254_4392))</code></td>
    </tr>
    <tr>
      <td>nsp12</td>
      <td>PRO_0000449629</td>
      <td>4393 – 5324</td>
      <td>RNA-directed RNA polymerase</td>
      <td><code class="language-plaintext highlighter-rouge">p(uniprot:P0DTD1 ! R1AB_SARS2, frag(4393_5324))</code></td>
    </tr>
    <tr>
      <td>nsp13</td>
      <td>PRO_0000449630</td>
      <td>5325 – 5925</td>
      <td>Helicase</td>
      <td><code class="language-plaintext highlighter-rouge">p(uniprot:P0DTD1 ! R1AB_SARS2, frag(5325_5925))</code></td>
    </tr>
    <tr>
      <td>nsp14</td>
      <td>PRO_0000449631</td>
      <td>5926 – 6452</td>
      <td>Proofreading exoribonuclease</td>
      <td><code class="language-plaintext highlighter-rouge">p(uniprot:P0DTD1 ! R1AB_SARS2, frag(5926_6452))</code></td>
    </tr>
    <tr>
      <td>nsp15</td>
      <td>PRO_0000449632</td>
      <td>6453 – 6798</td>
      <td>Uridylate-specific endoribonuclease</td>
      <td><code class="language-plaintext highlighter-rouge">p(uniprot:P0DTD1 ! R1AB_SARS2, frag(6453_6798))</code></td>
    </tr>
    <tr>
      <td>nsp16</td>
      <td>PRO_0000449633</td>
      <td>6799 – 7096</td>
      <td>2’-O-methyltransferase</td>
      <td><code class="language-plaintext highlighter-rouge">p(uniprot:P0DTD1 ! R1AB_SARS2, frag(6799_7096))</code></td>
    </tr>
  </tbody>
</table>

<p>I’m not sure what happened to #11. UniProt isn’t listing it here. There’s also
the Replicase polyprotein 1a, which lists nsp1-nsp11, but I’m not sure what the
difference is yet.</p>

<hr />

<p>When I first started writing this, I wasn’t actually aware of the existence of
the <code class="language-plaintext highlighter-rouge">uniprot.chain</code> entry in Identifiers.org. This makes things a lot better!
However, this leaves two tasks for me:</p>

<ol>
  <li>Integrate the <code class="language-plaintext highlighter-rouge">uniprot.chain</code> nomenclature into PyOBO such that identifiers
can be validated and easily resolved to names</li>
  <li>
    <p>Generate equivalence relationships in BEL linking the CURIE-named and
ontologically-defined versions of each as in:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>p(uniprot.chain:PRO_0000449619) equivalentTo p(uniprot:P0DTD1 ! R1AB_SARS2, frag(1_180))
...
p(uniprot.chain:PRO_0000449633) equivalentTo p(uniprot:P0DTD1 ! R1AB_SARS2, frag(6799_7096))
</code></pre></div>    </div>
  </li>
</ol>

<p>Happy BEL coding!</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="curation" /><category term="BEL" /><summary type="html"><![CDATA[Many of the proteins in the severe acute respiratory syndrome coronavirus 2 (SARS-CoV-2) are cleavage products of the replicase polyprotein 1ab (uniprot:P0DTD1). Unfortunately, the bioinformatics community is not so comfortable with proteins like this and nomenclature remains tricky. Luckily, the Biological Expression Language (BEL) has exactly the right tool to encode information about these proteins using the fragment() function.]]></summary></entry><entry><title type="html">How to Code with Me - Making a CLI</title><link href="https://cthoyt.com/2020/06/11/click.html" rel="alternate" type="text/html" title="How to Code with Me - Making a CLI" /><published>2020-06-11T15:48:00+00:00</published><updated>2020-06-11T15:48:00+00:00</updated><id>https://cthoyt.com/2020/06/11/click</id><content type="html" xml:base="https://cthoyt.com/2020/06/11/click.html"><![CDATA[<p>One of the cardinal sins in computational science is to hard code a file path in
your analysis. This post is a guide to reorganizing your code to avoid this and
then to generate a command line interface (CLI) using
<a href="https://click.palletsprojects.com/en/7.x/">click</a>.</p>

<p>The best way around this is to make all of your code live inside a function that
takes a file path as an argument. Here’s an example of some sinful code:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># sinful_analysis.py
</span><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>

<span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s">'/Users/cthoyt/data/example.tsv'</span><span class="p">)</span>
<span class="n">analysis</span> <span class="o">=</span> <span class="n">do_analysis</span><span class="p">(</span><span class="n">df</span><span class="p">)</span>
<span class="n">save_analysis</span><span class="p">(</span><span class="n">analysis</span><span class="p">,</span> <span class="s">'/Users/cthoyt/data/analysis.tsv'</span><span class="p">)</span>
</code></pre></div></div>

<p>Here’s the same code, but enlightened:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># enlightened_analysis.py
</span><span class="kn">import</span> <span class="nn">pandas</span>

<span class="k">def</span> <span class="nf">do_enlightened_analysis</span><span class="p">(</span><span class="n">input_path</span><span class="p">,</span> <span class="n">output_path</span><span class="p">):</span>
    <span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="n">input_path</span><span class="p">)</span>
    <span class="n">analysis</span> <span class="o">=</span> <span class="n">do_analysis</span><span class="p">(</span><span class="n">df</span><span class="p">)</span>
    <span class="n">save_analysis</span><span class="p">(</span><span class="n">analysis</span><span class="p">,</span> <span class="n">output_path</span><span class="p">)</span>
</code></pre></div></div>

<p>The enlightened code doesn’t contain any references to the file paths on which
you’re doing analysis. In fact, the enlightened code can’t even be run directly
without passing the file paths as variables. This pattern gets you in the
mindset of separating the code from the configuration for running the code.
Again, this is important because the file path will change depending on who’s
running it, if you decide to do spring cleaning on your hard drive, or if you
get new files.</p>

<p>There are lots of ways you might pass the input and output paths into this
function. The most obvious, since you’ve probably read my <a href="/2020/06/03/how-to-code-with-me-organization.html">previous blog
post</a> and you’re now
a packaging master, is to import <code class="language-plaintext highlighter-rouge">enlightened_analysis</code> and run it from the
Python REPL. Another way would be to make a one-off Python script whose job is
to actually run the analysis (as opposed to this example, which is creating the
workflow to be run). Though this are both better than the sinful analysis, it’s
a problem since you have to manually interact with Python to run your code.</p>

<p>You’re likely familiar with using the CLI for <code class="language-plaintext highlighter-rouge">pip</code>. Wouldn’t it be terrible if
you had to write a Python script that calls <code class="language-plaintext highlighter-rouge">pip</code> (like R makes you do with
<code class="language-plaintext highlighter-rouge">install.packages()</code>, ughhh!!). This is the same visceral reaction you should
have to having to make specific python code for an analysis.</p>

<h2 id="making-your-first-cli">Making your first CLI</h2>

<p>As you might have guessed, the solution is to make a CLI. After making your
function that does the hard work, the job of the CLI should just take care of
getting the configuration (e.g., file paths) from the user and passing them to
your functions that do the hard work.</p>

<p>CLIs should be very very short! If you’re putting lots of logic inside your CLI,
then you should probably reconsider refactoring that logic into more generally
reusable functions. Here’s an example of the hello world CLI:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># cli_simple.py
</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'hello'</span><span class="p">)</span>
</code></pre></div></div>

<p>Then you run this python script with <code class="language-plaintext highlighter-rouge">python cli_simple.py</code>. It’s boring. You
didn’t need to read this guide to do this. However, you might not know what
<code class="language-plaintext highlighter-rouge">if __name__ == '__main__'</code> is. It turns out that all python files know their
name and store it in the <code class="language-plaintext highlighter-rouge">__name__</code> variable when they’re <em>imported</em>. However,
if you are running a python file as a <em>script</em> then <code class="language-plaintext highlighter-rouge">__name__</code> gets set to the
string <code class="language-plaintext highlighter-rouge">'__main__'</code>. This allows you to make sure that the <code class="language-plaintext highlighter-rouge">print('hello')</code> is
only ever run if the user is actually running the script as a CLI.</p>

<p>However, don’t be tempted to put lots of code in <code class="language-plaintext highlighter-rouge">if __name__ == '__main__'</code>.
You should always make a function <code class="language-plaintext highlighter-rouge">main()</code> where all of the code that runs the
CLI, and just call it.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># cli_simple_2.py
</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'hello'</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<p>You still run this script with <code class="language-plaintext highlighter-rouge">python cli_simple_2.py</code>. Another reason we
introduced <code class="language-plaintext highlighter-rouge">main()</code> is to take care of getting information from the user. That’s
where <code class="language-plaintext highlighter-rouge">click</code> comes in. It makes functions do all sorts of magical things to get
information from command line arguments. All you have to do is use function
decorator (something starting with the <code class="language-plaintext highlighter-rouge">@</code> symbol) to annotate that the function
is a <code class="language-plaintext highlighter-rouge">click.command()</code>. If you’re not already familiar with decorators, check
<a href="https://www.youtube.com/watch?v=MjHpMCIvwsY">this short video</a> or
<a href="https://www.youtube.com/watch?v=81S01c9zytE">this long video</a>.</p>

<p>Run <code class="language-plaintext highlighter-rouge">pip install click</code> in your shell then update your code to look like this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># cli_simple_3.py
</span><span class="kn">import</span> <span class="nn">click</span>

<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">command</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'hello'</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<p>You still run this script with <code class="language-plaintext highlighter-rouge">python cli_simple_3.py</code> and it does exactly the
same as the last one. However, once your main function is a <code class="language-plaintext highlighter-rouge">click.command()</code>,
you can do all sorts of wonderful things. The first is to pass arguments from
the command line into the function. Update your code to look like the following:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># cli_simple_4.py
</span><span class="kn">import</span> <span class="nn">click</span>

<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">command</span><span class="p">()</span>
<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">argument</span><span class="p">(</span><span class="s">'text'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">text</span><span class="p">):</span>
    <span class="k">print</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<p>You probably notice that the call to <code class="language-plaintext highlighter-rouge">main()</code> at the bottom does not include an
argument for <code class="language-plaintext highlighter-rouge">text</code>. That’s because <code class="language-plaintext highlighter-rouge">click</code> is decorating the original function
in the meantime, which means that the actual thing called <code class="language-plaintext highlighter-rouge">main</code> is a function
that takes no arguments in the end. This is good, because click uses the extra
decorators (e.g. <code class="language-plaintext highlighter-rouge">click.argument(...)</code>, <code class="language-plaintext highlighter-rouge">click.option(...)</code>) to figure out what
to put in the arguments from the function that we actually wrote. Notice that
the <code class="language-plaintext highlighter-rouge">@click.argument('text')</code> matches up to the variable name. That’s no
coincidence.</p>

<p>You can now run this script from the command line with <code class="language-plaintext highlighter-rouge">python cli_simple_4.py</code>.
You’ll see that it yells at you for forgetting the <code class="language-plaintext highlighter-rouge">text</code> argument. Better not!
Try again with <code class="language-plaintext highlighter-rouge">python cli_simple_4.py "Hello World!"</code> and you’ll be happy to
see you’re now at <em>Hello World</em> for CLIs. From here you can do all sorts of
stuff which is all outlined in the excellent
<a href="https://click.palletsprojects.com/en/7.x/"><code class="language-plaintext highlighter-rouge">click</code> documentation</a>.</p>

<p><code class="language-plaintext highlighter-rouge">click</code> also automatically generates documentation for you, so it’s always
possible to run the command without arguments and with the <code class="language-plaintext highlighter-rouge">--help</code> flag as in
<code class="language-plaintext highlighter-rouge">python cli_simple_4.py --help</code>. It will give you information about all of the
arguments, their types, and more.</p>

<h2 id="clis-in-package-world">CLIs in Package World</h2>

<p>It’s my strong opinion that almost all code should be packaged, and the CLI is
no exception. To finish our original problem, we’ll create a python file
<code class="language-plaintext highlighter-rouge">cli.py</code> in the package where <code class="language-plaintext highlighter-rouge">enlightened_analysis.py</code> is and import our
function from there. Then we’ll add the right arguments to <code class="language-plaintext highlighter-rouge">click</code>, pass them to
the right place, and profit!</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># cli.py
</span><span class="kn">import</span> <span class="nn">click</span>
<span class="kn">from</span> <span class="nn">.enlightened_analysis</span> <span class="kn">import</span> <span class="n">do_enlightened_analysis</span>

<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">command</span><span class="p">()</span>
<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">argument</span><span class="p">(</span><span class="s">'input_path'</span><span class="p">)</span>
<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">argument</span><span class="p">(</span><span class="s">'output_path'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">input_path</span><span class="p">,</span> <span class="n">output_path</span><span class="p">):</span>
    <span class="n">do_enlightened_analysis</span><span class="p">(</span><span class="n">input_path</span><span class="p">,</span> <span class="n">output_path</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<p>If you’ve done it right, a the body of the <code class="language-plaintext highlighter-rouge">main()</code> function for your CLI should
very boring function. Of course, there are other ways to organize your code, but
this is a good way to do it until you’re more comfortable.</p>

<p>However, now we’re living in package world. In this tutorial, I’ve skipped the
explanation of turning the <code class="language-plaintext highlighter-rouge">enlightened_analysis.py</code> and <code class="language-plaintext highlighter-rouge">cli.py</code> into a
package. I’ll assume from here that you’ve done this and named the package
<code class="language-plaintext highlighter-rouge">superanalysis</code>. If you’re not familiar with doing that, check my <a href="/2020/06/03/how-to-code-with-me-organization.html">previous blog
post</a> or
<a href="https://www.youtube.com/watch?v=lo_g-GbYtaA">my tutorial on YouTube</a>.</p>

<p>We don’t want to interact with this code by running it as a script with
<code class="language-plaintext highlighter-rouge">python cli.py</code>. Instead, we want to interact with the code via the package.
Further, if you <code class="language-plaintext highlighter-rouge">cd</code> into the place where the code is and run <code class="language-plaintext highlighter-rouge">python cli.py</code>,
you’ll get an import warning because relative imports don’t work when you’re not
in a Python package context. This error is a good thing - it’s a reminder that
you should always live in the packaged world.</p>

<p>The solution to the problem is to use the <code class="language-plaintext highlighter-rouge">-m</code> flag in the <code class="language-plaintext highlighter-rouge">python</code> CLI.
Remember that <code class="language-plaintext highlighter-rouge">enlightened_analysis.py</code> and <code class="language-plaintext highlighter-rouge">cli.py</code> modules are in a package
called <code class="language-plaintext highlighter-rouge">superanalysis</code> (that you should have also already installed). You can
now run the CLI using <code class="language-plaintext highlighter-rouge">python -m superanalysis.cli &lt;input_path&gt; &lt;output_path&gt;</code>.
This is also going to set <code class="language-plaintext highlighter-rouge">__name__</code> to <code class="language-plaintext highlighter-rouge">'__main__'</code> the same way as running it
as a script, but you’re in the python package context!</p>

<h2 id="vanity-is-a-virtue">Vanity is a Virtue</h2>

<p>The <code class="language-plaintext highlighter-rouge">-m</code> can almost be used to run any python file inside your package as
command line interface, which means you should always wrap up code for the CLI
in <code class="language-plaintext highlighter-rouge">if __name__ == '__main__'</code> so it doesn’t accidentally get run if the module
is imported.</p>

<p>The exception is the <code class="language-plaintext highlighter-rouge">__init__.py</code> files can’t be run as a module. If you were
to write <code class="language-plaintext highlighter-rouge">python -m superanalysis</code>, it wouldn’t run the <code class="language-plaintext highlighter-rouge">__init__.py</code> file as a
script and instead would throw an error. If you want to associate a CLI with the
package , you need to make an additional file called <code class="language-plaintext highlighter-rouge">__main__.py</code> sitting next
to <code class="language-plaintext highlighter-rouge">cli.py</code> in the <code class="language-plaintext highlighter-rouge">superanalysis</code> package. As an aside, this also works in
subpackages.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># __main__.py
</span>
<span class="s">"""Entrypoint module, in case you use `python -m superanalysis`.

Why does this file exist, and why `__main__`? For more info, read:

 - https://www.python.org/dev/peps/pep-0338/
 - https://docs.python.org/3/using/cmdline.html#cmdoption-m
"""</span>

<span class="kn">from</span> <span class="nn">.cli</span> <span class="kn">import</span> <span class="n">main</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<p>This python module simply reuses the main function we already wrote before. It
can basically be copied verbatim from package to package, but don’t forget to
change the first line to match yours! I like to copy it because it also has the
information from the python docs on why it works.</p>

<p>Now, you can run <code class="language-plaintext highlighter-rouge">python -m superanalysis</code> instead of
<code class="language-plaintext highlighter-rouge">python -m superanalysis.cli</code>. We can also do one better. Wouldn’t it be nice to
make a CLI function so we could just run
<code class="language-plaintext highlighter-rouge">superanalysis &lt;input_path&gt; &lt;output_path&gt;</code>? You’re in luck, because since we
grouped all of our code in a <code class="language-plaintext highlighter-rouge">main()</code> function, we can make a small addition to
the <code class="language-plaintext highlighter-rouge">setup.cfg</code>’s entry points to tell <code class="language-plaintext highlighter-rouge">pip</code> to automatically create a
<code class="language-plaintext highlighter-rouge">superanalysis</code> CLI in your shell by doing the following:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[options.entry_points]</span>
<span class="py">console_scripts</span> <span class="p">=</span>
    <span class="py">superanalysis</span> <span class="p">=</span> <span class="s">superanalysis.cli:main</span>
</code></pre></div></div>

<p>The left part is the name of the CLI that will be in your shell, then the right
part has the path to a module followed by a colon <code class="language-plaintext highlighter-rouge">:</code> then the name of the
function to be run.</p>

<p>That’s pretty much it! Now you can make beautiful command line interfaces.
There’s one more topic that I think is worth noting at the end of this tutorial,
and that’s to use command line groups. This allows you to organize subcommands
in your CLI and import other CLIs from other modules. It would look like this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># cli.py
</span>
<span class="kn">import</span> <span class="nn">click</span>

<span class="o">@</span><span class="n">click</span><span class="p">.</span><span class="n">group</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="k">pass</span>  <span class="c1"># becuase this is a group, you don't actually need to do anything in it
</span>
<span class="o">@</span><span class="n">main</span><span class="p">.</span><span class="n">command</span><span class="p">()</span>  <span class="c1"># note that the main function now can assign commands
</span><span class="k">def</span> <span class="nf">subcommand1</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'hello world'</span><span class="p">)</span>

<span class="o">@</span><span class="n">main</span><span class="p">.</span><span class="n">command</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">subcommand2</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'other greeting'</span><span class="p">)</span>

<span class="o">@</span><span class="n">main</span><span class="p">.</span><span class="n">group</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">subgroup</span><span class="p">():</span>
    <span class="k">pass</span>

<span class="o">@</span><span class="n">subgroup</span><span class="p">.</span><span class="n">command</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">turtle</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'you can go as deep as you want with subcommands'</span><span class="p">)</span>

<span class="c1"># You can include other CLIs from other modules in your package
# to make everything much more unified
</span><span class="kn">from</span> <span class="nn">.my_other_module.cli</span> <span class="kn">import</span> <span class="n">main</span> <span class="k">as</span> <span class="n">other_module_command</span>
<span class="n">main</span><span class="p">.</span><span class="n">add_command</span><span class="p">(</span><span class="n">other_module_command</span><span class="p">)</span>
</code></pre></div></div>

<p>You can see that rather than using the <code class="language-plaintext highlighter-rouge">click.command()</code> decorator, the <code class="language-plaintext highlighter-rouge">main()</code>
got the <code class="language-plaintext highlighter-rouge">click.group()</code> decorator. This means that it can be used to issue
subcommands or even subgroups! At the end, it was also used to combine CLIs from
another part of the same package. This is good if you have a package that does
lots of things, but you want a single unified CLI to access all of it. Just be
careful with how the functions are named (it doesn’t always have to be main)
because if two are called the same thing then there will be a name clash and one
sub-command or sub-group won’t get shown.</p>

<hr />

<p>CLIs are really powerful! In the end, you can write in your README how you used
your CLI on your data to run your experiment. This gives others the best shot at
reproducing your work. Happy hunting, and see you the next installment of “How
to Code with Me”!</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="code with me" /><summary type="html"><![CDATA[One of the cardinal sins in computational science is to hard code a file path in your analysis. This post is a guide to reorganizing your code to avoid this and then to generate a command line interface (CLI) using click.]]></summary></entry><entry><title type="html">The Curation of Neurodegeneration Supporting Ontology</title><link href="https://cthoyt.com/2020/06/09/conso.html" rel="alternate" type="text/html" title="The Curation of Neurodegeneration Supporting Ontology" /><published>2020-06-09T19:31:00+00:00</published><updated>2020-06-09T19:31:00+00:00</updated><id>https://cthoyt.com/2020/06/09/conso</id><content type="html" xml:base="https://cthoyt.com/2020/06/09/conso.html"><![CDATA[<p>While I led the curation program in the
<a href="https://pharmacome.github.io">Human Brain Pharmacome</a> project during my Ph.D.
from 2018-2019 at Fraunhofer, we built the Curation of Neurodegeneration
Supporting Ontology (CONSO). This post outlines the project’s needs for quality
control and re-curation that lead to its generation, the curation process, and
how CONSO constitutes an example of how to follow the guidelines I proposed in a
<a href="/2020/05/12/building-an-ontology.html">previous blog post</a> on building
ontologies.</p>

<p>Before I joined Fraunhofer for my master’s and Ph.D., previous group members had
made first attempts at generating several narrow ontologies, terminologies, and
controlled vocabularies within the field of neurodegenerative disease research
which were then used as controlled vocabularies (CVs) in knowledge graphs
curated for the <a href="https://www.aetionomy.eu">AETIONOMY</a> project. However, the
project did not outline a maintenance plan and around late 2015, AETIONOMY’s
curation program effectively ended.</p>

<p>Three years later (at the start of 2018), I was tasked with revitalizing the
curation program as part of the
<a href="https://pharmacome.github.io">Human Brain Pharmacome</a> project. This meant
assessing the quality and currentness of knowledge graphs and their underlying
CVs before leading a new curation campaign to enrich them (to be described in a
future blog post). The remainder of this blog post is about the issues with
these underlying ontologies, how we addressed them, and a description of the
resulting ontology that we generated.</p>

<h2 id="assessing-controlled-vocabularies-used-by-neurommsig">Assessing Controlled Vocabularies used by NeuroMMSig</h2>

<p>The first step was to categorize the ontologies, terminologies, and CVs that
were used in the knowledge graphs (the set of which comprises NeuroMMSig
(<a href="https://academic.oup.com/bioinformatics/article/33/22/3679/3884654">paper</a>,
<a href="https://neurommsig.scai.fraunhofer.de/">website</a>) from now on) into high and
low confidence. The first criterion for high confidence was that they were
either in a repository of high quality nomenclatures (either
<a href="https://www.ebi.ac.uk/ols">OLS</a> or <a href="http://www.obofoundry.org/">OBO Foundry</a>)
or they were registered in the MIRIAM database at
<a href="https://identifiers.org">Identifiers.org</a>. The second criterion for high
confidence was that they were being actively maintained in a version controlled
repository like the
<a href="https://github.com/obophenotype/human-phenotype-ontology">Human Phenotype Ontology</a>
or <a href="https://github.com/sorgerlab/famplex">FamPlex</a>. Because there are a few
examples of standard ontologies that are not maintained, the confidence labels
help convey this distinction. As an aside, the difference between ontology,
terminology, and CV is not important for their application in the curation of
knowledge graphs (see my <a href="/2020/05/12/building-an-ontology.html">previous blog
post</a>). For the remainder of
this post, I’ll be calling them CVs.</p>

<p>The list of CVs used in NeuroMMSig that did not meet the high confidence
criteria included three previously published ontologies (i.e.,
<a href="https://doi.org/10.1016/j.jalz.2013.02.009">ADO</a>,
<a href="https://doi.org/10.1186/s12976-015-0017-y">PDON</a>,
<a href="https://doi.org/10.1371/journal.pone.0116718">MSO</a>) that could be found in the
Protégé OWL format through the
<a href="http://bioportal.bioontology.org/">NCBO BioPortal</a>. The underlying issue with
these three resources is that they are actually text mining dictionaries
masquerading as ontologies. As outlined in their respective publications, they
were created to train a dictionary-based named entity recognition system. From
there, careless usage of Protégé led to confusing organization, messy and
cryptic entity labels, lack of meaningful entity hierarchies, lack of
definitions or provenance for each entity, lack of curator provenance for each
entity, and non-compliance with the MIRIAM standards for entity identifiers.
Further, the lack of maintenance meant that when entities originally novel to
these resources were curated again for inclusion in high confidence, mainstream
ontologies, that there would exist no mappings (e.g., equivalences) to these
high confidence resources. This could create redundancies that undermine the
value of the downstream resources, like NeuroMMSig, that use these low
confidence resources. Like I outlined in my previous post, the solution to this
problem is to <strong>not</strong> compete with groups maintaining high confidence resources,
and instead to contribute to them directly.</p>

<p>A fourth previously published CV (i.e.,
<a href="https://doi.org/10.3233/jad-161148">NIFT</a>) in NeuroMMSig that did not meet the
high confidence criteria only existed as a
<a href="https://biological-expression-language.github.io/">Biological Expression Language (BEL)</a>
namespace file served by Fraunhofer’s web-based BEL curation interface. Because
it could be shut down at any time, I started by copying it to a
<a href="https://arty.scai.fraunhofer.de/artifactory/bel/namespace/nift/NIFT.belns">more persistent server</a>
but later began using GitHub directly for versioning and distribution. While not
masquerading as an ontology, this CV also had issues with messy names (inclusion
of strange punctuation, inconsistent usage of capitalization, etc.). By virtue
of its lack of structure or organization that benefits ontologies, the
assessment of each term for its novelty, correctness, and whether it could be
replaced with an already existing entry in a high confidence ontology became its
own time-consuming research project.</p>

<p>There were additional low confidence CVs generated as <em>ad-hoc</em> extensions to
high confidence CVs. For example, the Brain Region and Cell-type Ontology (BRCO;
unpublished) was an extension to <a href="https://uberon.github.io">Uberon</a>.
Unfortunately, the value of this extension was questionable as it seemed most
(if not all) of the extensions made actually duplicated efforts of the original
ontology itself. As several years had passed between the generation of this
extension and my assessment, it’s possible that some of the terms included in
BRCO were later curated by Uberon by virtue of its excellent maintenance. This
phenomenon persisted across other examples of <em>ad-hoc</em> ontology extensions I
found in NeuroMMSig.</p>

<p>Finally, there were several other novel low confidence CVs only distributed as
BEL namespaces that suffered from a mixture of the previously described
problems.</p>

<h2 id="making-a-home-for-lost-and-forgotten-names">Making a Home for Lost and Forgotten Names</h2>

<p>It should also be noted that my assessment of the low confidence CVs focused on
their respective terms that appeared in the NeuroMMSig knowledge graphs. There
were also hundreds of terms appearing in NeuroMMSig that had not been assigned
based on <em>any</em> CV at all (what I called “naked names”). Some could be assigned
via text mining tools, but others needed a home. It was finally time to start
curating an ontology that could unify all of the existing abandoned CVs used by
NeuroMMsig and solve their respective problems. And so, along with my team of
excellent students, work was begun on the Curation of Neurodegeneration
Supporting Ontology (CONSO; <a href="https://github.com/pharmacome/conso">data</a>,
<a href="https://pharmacome.github.io/conso/">website</a>).</p>

<p>We began by re-curating all of the terms from low confidence CVs that appeared
in NeuroMMSig. Each was given its own entry in CONSO along with a persistent
identifier, a typographically sane preferred label, a definition in english
prose, references to papers where it appeared, and contact information of the
curator in case there were questions. We found that the curator attribution was
incredibly important as questions arose about previous terms. We maintained
synonyms and xrefs for each term as well as implementing code that leverages
several services (e.g., OLS, PubChem, etc.) for new mappings to propose for
curation. After, we did the same for all of the names appearing in NeuroMMSig
that were not qualified with a namespace/prefix. Later, CONSO was improved with
completely novel curation to support additional curation of neurodegeneration
disease phenomena that became the Curation of Neurodegeneration in BEL (CONIB;
<a href="https://biological-expression-language.github.io/">data</a>,
<a href="https://pharmacome.github.io/conib/">website</a>).</p>

<p><img src="/img/conso_summary.png" alt="CONSO Contents" /></p>

<p>CONSO maintains entities of several types, including chemicals, chemical roles,
proteins, protein families, protein variants, protein isoforms, and protein
isoform families (see figure below). It also maintains its own vocabulary of
relationships and instances of those relationships. For example, we were unable
to find a relation that describes the epitope corresponding to a given antibody
in a high quality resource (e.g., the
<a href="http://www.obofoundry.org/ontology/ro.html">Relation Ontology</a>). Therefore, we
created our own relationship and started curating instances of it. Because it’s
going as far as defining its own semantics, CONSO is a proper ontology.</p>

<p>While CONSO’s hierarchy is currently neither aligned to the
<a href="http://www.obofoundry.org/ontology/bfo">Basic Formal Ontology</a> nor the
<a href="https://www.ebi.ac.uk/sbo/main/">Systems Biology Ontology</a> for the
classification of entity types, the current organizational scheme is best suited
to support downstream curation of biomedical relations. While I’ve written quite
a bit about the applications of terminologies and ontologies by this point, I am
definitely not an expert in its pedagogy and will have to bring in others for
discussion on how to do this best. One of the other reasons why I haven’t done
that yet is that we are slowly making pull requests, issues, and contributions
to other ontologies to move all of the best stuff out of CONSO into other,
better maintained ontologies. Like the resources that it supersedes, CONSO is
not meant to be a high confidence resource, but rather an intermediate stage for
curation as we find it a more appropriate home.</p>

<p align="center">
<img src="/img/conso_checker.png" alt="CONSO checker" width="350" />
</p>

<p>CONSO uses continuous integration on Travis-CI and an extensible verification
script to check the integrity of the ontology on each push. The example above
shows that isoforms are checked to have a relationship to their reference
protein and that chemicals should be annotated with InChI, SMILES, and also
InChI-key (not shown). There are also checks that all chemicals have been
assigned chemical roles from the ChEBI ontology or CONSO (we’ve curated a few
ourselves that get put in ChEBI later), that antibodies have been annotated with
their targets.</p>

<p>It also performs simple checks that new terms are added with the next
consecutive identifier, that minimum appropriate metadata has been added for new
terms (we set the bar <em>very</em> high), and that all sheets use valid CONSO
identifiers. Overall, these kinds of checks make it much easier to work in a
team. As CONSO was curated by eight authors, they lessened the burden on
explaining the rules for curation to new contributors - all changes must be made
as pull requests and they must pass the tests on Travis-CI. If they don’t, new
contributors just need to follow the instructions given to make updates until
they do.</p>

<p align="center">
<img src="/img/recuration_warnings.png" alt="CONSO Contents" width="350" />
</p>

<p>As the original goal of CONSO was to support the re-curation and enrichment of
NeuroMMSig, it’s nice to see in the above figure how its development allowed us
to reduce the number of BEL warnings in NeuroMMSig from almost 30,000 to zero
(in addition to some other BEL-specific fixes). This figure comes from and old
blog post on the Fraunhofer Human Brain Pharmacome site (which appears to have
been taken down?) and
<a href="https://docs.google.com/presentation/d/1Z3mNkphFABE4LyG3wpgzWQECEZ1N_cgvbLnTiAzAXpE/edit?usp=sharing">my Ph.D. defense</a> -
and will hopefully make it to primetime in the next release of NeuroMMSig
itself. Further, we were able to improve the overall quality of NeuroMMSig by
normalizing many terms that were redundant across namespaces as previously
described. CONSO was created using the guide in my <a href="/2020/05/12/building-an-ontology.html">previous blog
post</a>. More specifically, the
experiences with CONSO inspired that guide to highlight the things that worked
well (and not the things that didn’t). In the end, CONSO is automatically
converted into an
<a href="https://github.com/pharmacome/conso/blob/master/export/conso.owl">OWL file</a>, an
<a href="https://github.com/pharmacome/conso/blob/master/export/conso.obo">OBO file</a>, a
<a href="https://github.com/pharmacome/conso/blob/master/export/conso-names.belns">BEL namespace</a>
for BEL curation, and a <a href="https://pharmacome.github.io/conso/">website</a> for
search and exploration that is automatically deployed with GitHub Pages. It’s
archived in Zenodo as well at
<a href="https://doi.org/10.5281/zenodo.3886640"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.3886640.svg" alt="DOI" /></a>.</p>

<hr />

<p>CONSO could not have been done without all of the hard work of the curators at
Fraunhofer during the AETIONOMY project that provided the basis for this work.
However, I would like to make a special thanks to my curation team - Rana
Aldisi, Lingling Xu, Sandra Spalek, Esther Wollert, Kristian Kolpeja, and Yojana
Gadiya for their immense contributions, Daniel Domingo-Fernández for helping
when I needed it, and Stephan Gebel for learning so much so quickly in order to
take over the curation program after I finished my Ph.D. and left Fraunhofer.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="curation" /><category term="ontology" /><summary type="html"><![CDATA[While I led the curation program in the Human Brain Pharmacome project during my Ph.D. from 2018-2019 at Fraunhofer, we built the Curation of Neurodegeneration Supporting Ontology (CONSO). This post outlines the project’s needs for quality control and re-curation that lead to its generation, the curation process, and how CONSO constitutes an example of how to follow the guidelines I proposed in a previous blog post on building ontologies.]]></summary></entry><entry><title type="html">How to Code with Me - Organizing a Package</title><link href="https://cthoyt.com/2020/06/03/how-to-code-with-me-organization.html" rel="alternate" type="text/html" title="How to Code with Me - Organizing a Package" /><published>2020-06-03T08:00:00+00:00</published><updated>2020-06-03T08:00:00+00:00</updated><id>https://cthoyt.com/2020/06/03/how-to-code-with-me-organization</id><content type="html" xml:base="https://cthoyt.com/2020/06/03/how-to-code-with-me-organization.html"><![CDATA[<p>This blog post is the next installment in the series about all of the very
particular ways I do software development in Python. This round is about where
to put your code, your tests, your CLI, and the right metadata for each.</p>

<h2 id="package-structure">Package Structure</h2>

<p>After following the debate on
<a href="https://github.com/pypa/packaging.python.org/issues/320">pypa/packaging.python.org#320</a>,
I’ve opted to use the <code class="language-plaintext highlighter-rouge">src/</code> layout as aptly described by
<a href="https://blog.ionelmc.ro/2014/05/25/python-packaging/">Ionel Cristian Mărieș</a>,
and <a href="https://hynek.me/articles/testing-packaging/">Hynek Schlawack</a>.</p>

<p>This means that there’s a top-level <code class="language-plaintext highlighter-rouge">tests/</code> directory (will come back to that
later) and no possibility of mixing up your working directory when making
imports. I also enforce the sole usage of
<a href="https://realpython.com/absolute-vs-relative-python-imports/#relative-imports">relative imports</a>
to make sure there are no accidental circular imports. While I know this is
allowed and reasonable sometimes, I assume others will misuse it. Additionally,
relative imports force users to access their scripts like modules using the
command line like <code class="language-plaintext highlighter-rouge">python -m my_module.my_submodule</code>. This is good because I
believe there should be no such thing as Python scripts. You should always think
about packaging and how someone else will use your code later.</p>

<h2 id="licensing">Licensing</h2>

<p>Your package should have a file called LICENSE (no extension) that tells people
how they’re allowed to use your code. Even if you’re working in a company and
won’t be sharing code, it’s still good practice.</p>

<p>An excellent resource to help you choose a license is
<a href="https://choosealicense.com/">https://choosealicense.com/</a>. I normally pick
<a href="https://choosealicense.com/licenses/mit/">MIT License</a> because it’s easy for
other people to use and modify.</p>

<h2 id="ignore-the-junk">Ignore the Junk</h2>

<p>This repository uses a <code class="language-plaintext highlighter-rouge">.gitignore</code> file to make sure no junk gets committed.
GitHub will ask you if you want a pre-populated <code class="language-plaintext highlighter-rouge">.gitignore</code> added to your repo
on creation. You can also go to <a href="https://www.gitignore.io">gitignore.io</a> to get
more options.</p>

<p>Things that are especially bad to commit to repos:</p>

<ul>
  <li>compiled python files (<code class="language-plaintext highlighter-rouge">*.pyc</code>)</li>
  <li>Jupyter notebook checkpoint folders (<code class="language-plaintext highlighter-rouge">.ipynb_checkpoints/</code>)</li>
  <li>documentation builds (let ReadTheDocs take care of this!)</li>
  <li>tox and other automation/build tool caches (<code class="language-plaintext highlighter-rouge">.tox/</code>, <code class="language-plaintext highlighter-rouge">.pytest_cache/</code>,
<code class="language-plaintext highlighter-rouge">.mypy_cache/</code>, <code class="language-plaintext highlighter-rouge">build/</code>, <code class="language-plaintext highlighter-rouge">dist/</code>, etc.)</li>
  <li>basically any file you didn’t make on purpose</li>
</ul>

<p>I usually use
<a href="https://www.toptal.com/developers/gitignore/api/python,jupyternotebooks,intellij,pycharm,macos,linux,windows">this</a>
pre-configured <code class="language-plaintext highlighter-rouge">.gitignore</code> with has ignores appropriate for Python, Jupyter
Notebooks, IntelliJ/PyCharm, Mac, Windows, and Linux.</p>

<h2 id="packaging">Packaging</h2>

<p>I use a
<a href="https://setuptools.readthedocs.io/en/latest/setuptools.html">declarative setup</a>
in all of my packages. It’s not easy to figure out everything in this
documentation, so I either copy-paste from a previous project (usually
<a href="https://github.com/pybel/pybel/blob/master/setup.cfg">pybel/pybel</a> or use my
<a href="https://github.com/cthoyt/cookiecutter-snekpack">cookiecutter template</a>.</p>

<p>First, you need to create a <code class="language-plaintext highlighter-rouge">setup.py</code> file in the root of your repository when
using a declarative setup. It should always look exactly like this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># -*- coding: utf-8 -*-
</span>
<span class="s">"""The setup module."""</span>

<span class="kn">import</span> <span class="nn">setuptools</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">setuptools</span><span class="p">.</span><span class="n">setup</span><span class="p">()</span>
</code></pre></div></div>

<p>The first section in the <code class="language-plaintext highlighter-rouge">setup.cfg</code> is <code class="language-plaintext highlighter-rouge">[metadata]</code>. The
<a href="https://github.com/pybel/pybel/blob/dba0c5afd37bef7d162937d0407045f15a515a87/setup.cfg#L5-L8">top</a>
of the <code class="language-plaintext highlighter-rouge">setup.cfg</code> for PyBEL looks like this:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[metadata]</span>
<span class="c"># The name of the package (should be same as what's in `src/{your project name}`)
</span><span class="py">name</span> <span class="p">=</span> <span class="s">pybel</span>
<span class="c"># The version of the package (you should start with 0.0.1-dev for new projects)
</span><span class="py">version</span> <span class="p">=</span> <span class="s">0.14.6-dev</span>
<span class="c"># A one line description of your package, should be the same as the module-level docstring
# in src/{your project}/__init__.py
</span><span class="py">description</span> <span class="p">=</span> <span class="s">Parsing, validation, compilation, and data exchange of Biological Expression Language (BEL)</span>
<span class="c"># The `file:` magical prefix tells it to load what's in your README.rst. You did write a nice readme, right?
</span><span class="py">long_description</span> <span class="p">=</span> <span class="s">file: README.rst</span>
</code></pre></div></div>

<p>The
<a href="https://github.com/pybel/pybel/blob/dba0c5afd37bef7d162937d0407045f15a515a87/setup.cfg#L10-L16">next few lines</a>
describe the places where project resources live on the internet:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># [metadata]
# Where is the project
</span><span class="py">url</span> <span class="p">=</span> <span class="s">https://github.com/pybel/pybel</span>
<span class="c"># Where can people get your code
</span><span class="py">download_url</span> <span class="p">=</span> <span class="s">https://github.com/pybel/pybel/releases</span>
<span class="c"># You can put whatever key-values here you want, but these three are special
</span><span class="py">project_urls</span> <span class="p">=</span>
    <span class="err">Bug</span> <span class="py">Tracker</span> <span class="p">=</span> <span class="s">https://github.com/pybel/pybel/issues</span>
    <span class="err">Source</span> <span class="py">Code</span> <span class="p">=</span> <span class="s">https://github.com/pybel/pybel</span>
    <span class="py">Documentation</span> <span class="p">=</span> <span class="s">https://pybel.readthedocs.io</span>
</code></pre></div></div>

<p>Next is
<a href="https://github.com/pybel/pybel/blob/dba0c5afd37bef7d162937d0407045f15a515a87/setup.cfg#L18-L26">author and licensing information</a>.</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># [metadata]
# Author information
</span><span class="py">author</span> <span class="p">=</span> <span class="s">Charles Tapley Hoyt</span>
<span class="py">author_email</span> <span class="p">=</span> <span class="s">cthoyt@gmail.com</span>
<span class="c"># Who is actually taking care of the code? Might not be the same as the author
</span><span class="py">maintainer</span> <span class="p">=</span> <span class="s">Charles Tapley Hoyt</span>
<span class="py">maintainer_email</span> <span class="p">=</span> <span class="s">cthoyt@gmail.com</span>

<span class="c"># What kind of license are you using? This uses a SPDX identifier (https://spdx.org/licenses/)
</span><span class="py">license</span> <span class="p">=</span> <span class="s">MIT</span>
<span class="c"># The thing you put here is the name of the file in the same directory as the setup.cfg
</span><span class="py">license_file</span> <span class="p">=</span> <span class="s">LICENSE</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">license_file</code> entry obviously points to the file. The <code class="language-plaintext highlighter-rouge">license</code> entry is
what gets shown on PyPI using the
<a href="https://spdx.org/licenses/">Software Package Data Exchange</a> controlled
vocabulary.</p>

<p>Next comes the
<a href="https://github.com/pybel/pybel/blob/dba0c5afd37bef7d162937d0407045f15a515a87/setup.cfg#L28-L42">PyBEL classifiers</a>.
This is a list of <a href="https://pypi.org/classifiers/">trove classifiers</a> that are a
controlled vocabulary for describing your project’s development status, who
should use it, its topics, <em>etc</em>.</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># [metadata]
# Search tags
</span><span class="py">classifiers</span> <span class="p">=</span>
    <span class="err">Development</span> <span class="err">Status</span> <span class="err">::</span> <span class="err">5</span> <span class="err">-</span> <span class="err">Production/Stable</span>
    <span class="err">Environment</span> <span class="err">::</span> <span class="err">Console</span>
    <span class="err">Intended</span> <span class="err">Audience</span> <span class="err">::</span> <span class="err">Developers</span>
    <span class="err">Intended</span> <span class="err">Audience</span> <span class="err">::</span> <span class="err">Science/Research</span>
    <span class="err">License</span> <span class="err">::</span> <span class="err">OSI</span> <span class="err">Approved</span> <span class="err">::</span> <span class="err">MIT</span> <span class="err">License</span>
    <span class="err">Operating</span> <span class="err">System</span> <span class="err">::</span> <span class="err">OS</span> <span class="err">Independent</span>
    <span class="err">Programming</span> <span class="err">Language</span> <span class="err">::</span> <span class="err">Python</span>
    <span class="err">Programming</span> <span class="err">Language</span> <span class="err">::</span> <span class="err">Python</span> <span class="err">::</span> <span class="err">3.8</span>
    <span class="err">Programming</span> <span class="err">Language</span> <span class="err">::</span> <span class="err">Python</span> <span class="err">::</span> <span class="err">3.7</span>
    <span class="err">Programming</span> <span class="err">Language</span> <span class="err">::</span> <span class="err">Python</span> <span class="err">::</span> <span class="err">3.6</span>
    <span class="err">Programming</span> <span class="err">Language</span> <span class="err">::</span> <span class="err">Python</span> <span class="err">::</span> <span class="err">3.5</span>
    <span class="err">Programming</span> <span class="err">Language</span> <span class="err">::</span> <span class="err">Python</span> <span class="err">::</span> <span class="err">3</span> <span class="err">::</span> <span class="err">Only</span>
    <span class="err">Topic</span> <span class="err">::</span> <span class="err">Scientific/Engineering</span> <span class="err">::</span> <span class="err">Bio-Informatics</span>
    <span class="err">Topic</span> <span class="err">::</span> <span class="err">Scientific/Engineering</span> <span class="err">::</span> <span class="err">Chemistry</span>
</code></pre></div></div>

<p>Again, the license is very important! <code class="language-plaintext highlighter-rouge">pyroma</code> (see below) won’t pass if you
don’t have this. Also, the other things are important too, because this will
tell users that you’re cool and only allow the newest Python versions.
Unfortunately, at the time of writing this post, I still had to support Python
3.5 in PyBEL for downstream dependencies :/</p>

<p>Next are the
<a href="https://github.com/pybel/pybel/blob/dba0c5afd37bef7d162937d0407045f15a515a87/setup.cfg#L43-L49">keywords</a>,
which can be whatever you want. Here’s what I’ve got for PyBEL:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># [metadata]
</span><span class="py">keywords</span> <span class="p">=</span>
    <span class="err">Biological</span> <span class="err">Expression</span> <span class="err">Language</span>
    <span class="err">BEL</span>
    <span class="err">Domain</span> <span class="err">Specific</span> <span class="err">Language</span>
    <span class="err">DSL</span>
    <span class="err">Systems</span> <span class="err">Biology</span>
    <span class="err">Networks</span> <span class="err">Biology</span>
</code></pre></div></div>

<p>Next is the <code class="language-plaintext highlighter-rouge">[options]</code> section. First we’ll tell it what the requirements for
the package are:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[options]</span>
<span class="py">install_requires</span> <span class="p">=</span>
    <span class="err">networkx&gt;=2.1</span>
    <span class="err">sqlalchemy</span>
    <span class="err">click</span>
    <span class="err">click-plugins</span>
    <span class="err">bel_resources&gt;=0.0.3</span>
    <span class="err">more_itertools</span>
    <span class="err">requests</span>
    <span class="err">requests_file</span>
    <span class="err">pyparsing</span>
    <span class="err">tqdm</span>
</code></pre></div></div>

<p>This is part that’s hard to explain. The <code class="language-plaintext highlighter-rouge">packages</code> and <code class="language-plaintext highlighter-rouge">package_dir</code> option are
tricky… you just have to do it this way and everything magically works. Then,
you basically say the same thing one more time in the <code class="language-plaintext highlighter-rouge">[options.packages.find]</code>.</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># [options]
# You're always supposed to set zip_safe = false
</span><span class="py">zip_safe</span> <span class="p">=</span> <span class="s">false</span>
<span class="c"># If you have some non-python files inside your `src/{your package/` directory you
# want to come for the ride when other people use your code, do this
</span><span class="py">include_package_data</span> <span class="p">=</span> <span class="s">True</span>
<span class="c"># Always tell people what python you support! Is redundant of classifiers, but that's how it is.
</span><span class="py">python_requires</span> <span class="p">=</span> <span class="s">&gt;=3.5</span>

<span class="c"># Where is my code?
</span><span class="py">packages</span> <span class="p">=</span> <span class="s">find:</span>
<span class="py">package_dir</span> <span class="p">=</span>
    <span class="err">=</span> <span class="err">src</span>

<span class="nn">[options.packages.find]</span>
<span class="py">where</span> <span class="p">=</span> <span class="s">src</span>
</code></pre></div></div>

<h2 id="testing-the-prequel-series">Testing (The Prequel Series)</h2>

<p>To make a tiny little test that shows everything works, make the following file
in <code class="language-plaintext highlighter-rouge">tests/test_import.py</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># -*- coding: utf-8 -*-
</span>
<span class="s">"""Test the module can be imported."""</span>

<span class="kn">import</span> <span class="nn">unittest</span>

<span class="k">class</span> <span class="nc">TestImport</span><span class="p">(</span><span class="n">unittest</span><span class="p">.</span><span class="n">TestCase</span><span class="p">):</span>
    <span class="s">"""A test case for import tests."""</span>

    <span class="k">def</span> <span class="nf">test_import</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="s">"""Test that PyBEL can be imported."""</span>
        <span class="kn">import</span> <span class="nn">pybel</span>
</code></pre></div></div>

<p>Now you’re ready to run some tests. Do the following in your shell:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>pytest
pytest tests/
</code></pre></div></div>

<p>Because <code class="language-plaintext highlighter-rouge">pytest</code> isn’t actually a requirement to use <code class="language-plaintext highlighter-rouge">pybel</code>, but it’s useful to
have installed, you can specify it in an optional requirement in the
<code class="language-plaintext highlighter-rouge">[options.extras_require]</code> section of your <code class="language-plaintext highlighter-rouge">setup.cfg</code> like in:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[options.extras_require]</span>
<span class="py">testing</span> <span class="p">=</span>
    <span class="err">pytest</span>
<span class="py">docs</span> <span class="p">=</span>
    <span class="err">sphinx</span>
    <span class="err">sphinx-rtd-theme</span>
    <span class="err">sphinx-click</span>
    <span class="err">sphinx-autodoc-typehints</span>
</code></pre></div></div>

<p>You can install PyBEL with the testing and docs extras like:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install</span> <span class="nt">-e</span> .[testing,docs]
</code></pre></div></div>

<p>Then you wouldn’t have to worry about the availability of <code class="language-plaintext highlighter-rouge">pytest</code>. However,
there’s a better way to make sure that <code class="language-plaintext highlighter-rouge">pytest</code> is available, and more generally
for any testing or build task, we have <code class="language-plaintext highlighter-rouge">tox</code>.</p>

<h2 id="building-with-tox">Building with Tox</h2>

<p>There are three parts to automated builds with <code class="language-plaintext highlighter-rouge">tox</code>. First, you have to
<code class="language-plaintext highlighter-rouge">pip install tox</code>. Second, you have to make a file in the root of the repository
called <code class="language-plaintext highlighter-rouge">tox.ini</code> like below:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># The name of the default tox environment is [testenv]
</span><span class="nn">[testenv]</span>
<span class="c"># This is a list of commands to run as if you were in the shell yourself
</span><span class="py">commands</span> <span class="p">=</span>
    <span class="err">pytest</span> <span class="err">tests/</span>
<span class="c"># This is a list of extra dependencies to install with pip just for this testing environment
</span><span class="py">deps</span> <span class="p">=</span>
    <span class="err">pytest</span>
<span class="py">description</span> <span class="p">=</span> <span class="s">Run the tests using pytest.</span>
</code></pre></div></div>

<p>Third, you just have to run <code class="language-plaintext highlighter-rouge">tox</code> when the working directory in your shell is in
the root of the repository. Then everything is taken care of for you! <code class="language-plaintext highlighter-rouge">tox</code>
makes a new virtual environment, installs the repository using the
setup.cfg/setup.py, installs the tox environment-specific dependencies, then
runs the commands in order. There are tons of other options available for
customizing tox listed on their
<a href="https://tox.readthedocs.io/en/latest/">documentation</a>. There will be several
more examples here that show some of them being used.</p>

<p>Since you’re using the <code class="language-plaintext highlighter-rouge">extras</code> in the <code class="language-plaintext highlighter-rouge">setup.cfg</code>, you can actually rewrite
this configuration to use them:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[testenv]</span>
<span class="py">commands</span> <span class="p">=</span>
    <span class="err">pytest</span> <span class="err">tests/</span>
<span class="py">extras</span> <span class="p">=</span>
    <span class="err">testing</span>
</code></pre></div></div>

<h2 id="packaging-metadata">Packaging Metadata</h2>

<p>I use <code class="language-plaintext highlighter-rouge">pyroma</code> to make sure that I remembered to put everything in the packaging
metadata. It can be run with</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">python -m pip install pyroma</code></li>
  <li><code class="language-plaintext highlighter-rouge">pyroma --min=10 .</code></li>
</ol>

<p>I welcome and encourage you to copy my configuration, but don’t forget to
carefully change everything to your metadata. It’s pretty embarrassing if you
accidentally attribute your work to me. I’ve done it before by accident. I’ve
seen others do it too…</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[testenv:pyroma]</span>
<span class="py">deps</span> <span class="p">=</span>
    <span class="err">pygments</span>
    <span class="err">pyroma</span>
<span class="py">skip_install</span> <span class="p">=</span> <span class="s">true</span>
<span class="py">commands</span> <span class="p">=</span> <span class="s">pyroma --min=10 .</span>
<span class="py">description</span> <span class="p">=</span> <span class="s">Run the pyroma tool to check the package friendliness of the project.</span>
</code></pre></div></div>

<p>This environment adds the <code class="language-plaintext highlighter-rouge">skip_install</code> key, which just says not to bother pip
installing the whole package for the tests. This makes sense here because
checking the metadata contained in <code class="language-plaintext highlighter-rouge">setup.cfg</code> doesn’t require actually
installing the code.</p>

<h2 id="where-to-put-persistent-data">Where to Put Persistent Data</h2>

<p>If your application needs to download data that is not related to user input or
configuration, it’s best that it has a default location for storing stuff that
isn’t in a place a normal user will delete or corrupt. This is a situation where
it might make sense to make a folder in the user’s home directory.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># -*- coding: utf-8 -*-
</span>
<span class="s">"""Constants for PyBEL."""</span>

<span class="kn">import</span> <span class="nn">os</span>

<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s">'PYBEL_HOME'</span><span class="p">,</span>
<span class="p">]</span>

<span class="c1"># Have a reasonable default location
</span><span class="n">_DEFAULT_HOME</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">expanduser</span><span class="p">(</span><span class="s">'~'</span><span class="p">),</span> <span class="s">'.pybel'</span><span class="p">)</span>
<span class="c1"># Allow the user to modify the location with an environment variable
</span><span class="n">PYBEL_HOME</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">abspath</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">getenv</span><span class="p">(</span><span class="s">'PYBEL_HOME'</span><span class="p">,</span> <span class="n">_DEFAULT_HOME</span><span class="p">))</span>
<span class="n">os</span><span class="p">.</span><span class="n">makedirs</span><span class="p">(</span><span class="n">PYBEL_HOME</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>

<p>Now you can import <code class="language-plaintext highlighter-rouge">PYBEL_HOME</code> and do all sorts of nice <code class="language-plaintext highlighter-rouge">os.path.join</code>s to
build the directory structure to help organize the data you might need. For
example, PyBEL will download a copy of Daniel Himmelstein’s
<a href="https://github.com/hetio/hetionet">hetionet</a> for conversion to BEL and put it
in its cache folder.</p>

<p>Becuase this kind of configuration is so ubiquitous, I’ve written an package
that supports doing this called <a href="https://github.com/cthoyt/pystow"><code class="language-plaintext highlighter-rouge">pystow</code></a>
that simplifies the previous code to:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pystow</span>

<span class="n">pybel_module</span> <span class="o">=</span> <span class="n">pystow</span><span class="p">.</span><span class="n">module</span><span class="p">(</span><span class="s">"pybel"</span><span class="p">)</span>
<span class="n">PYBEL_HOME</span> <span class="o">=</span> <span class="n">pybel_module</span><span class="p">.</span><span class="n">base</span>
</code></pre></div></div>

<h2 id="where-to-put-configuration">Where to Put Configuration</h2>

<p>Only use the following section if your package <em>actually</em> needs configuration.
Keep in mind that there should always be reasonable defaults for anything that
can be configured, so users don’t actually have to engage with configuration.</p>

<p>Most packages put configuration inside the <code class="language-plaintext highlighter-rouge">~/.config/</code> folder, so you should do
the same. PyBEL uses something a bit different, but here’s a mock of how loading
configuration might look.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">from</span> <span class="nn">configparser</span> <span class="kn">import</span> <span class="n">ConfigParser</span>

<span class="n">CONFIG_PATH</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">expanduser</span><span class="p">(</span><span class="s">'~'</span><span class="p">),</span> <span class="s">'.config'</span><span class="p">,</span> <span class="s">'pybel.ini'</span><span class="p">)</span>

<span class="n">cfp</span> <span class="o">=</span> <span class="n">ConfigParser</span><span class="p">()</span>
<span class="n">cfp</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">CONFIG_PATH</span><span class="p">)</span>

<span class="k">try</span><span class="p">:</span>
    <span class="n">config</span> <span class="o">=</span> <span class="n">cfp</span><span class="p">[</span><span class="s">'pybel'</span><span class="p">]</span>
<span class="k">except</span> <span class="nb">KeyError</span><span class="p">:</span>
    <span class="n">config</span> <span class="o">=</span> <span class="p">{}</span>


<span class="k">def</span> <span class="nf">get_config</span><span class="p">(</span><span class="n">key</span><span class="p">):</span>
    <span class="s">"""Get PyBEL-specific configuration."""</span>
    <span class="k">return</span> <span class="n">config</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
</code></pre></div></div>

<p>You could also use JSON, but ini/cfg files are ubiquitous for configuration so
it’s best to stick to what’s expected. Because I’ve written the previous code so
often, I encapsulated it in a function in <code class="language-plaintext highlighter-rouge">pystow</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pystow</span>

<span class="k">def</span> <span class="nf">get_config</span><span class="p">(</span><span class="n">key</span><span class="p">):</span>
    <span class="n">pystow</span><span class="p">.</span><span class="n">get_config</span><span class="p">(</span><span class="s">"pybel"</span><span class="p">,</span> <span class="n">key</span><span class="p">)</span>
</code></pre></div></div>

<p>Note, this function has a few more bells and whistles than the boilerplate code
for fallbacks, passthroughs, error handling, and type coercion.</p>

<h2 id="code-style">Code Style</h2>

<p>Mercilessly use <code class="language-plaintext highlighter-rouge">flake8</code> to check your code has good style. If your code doesn’t
have good style, nobody else will be able to read it. I already wrote a whole
blog post on this one called <a href="/2020/04/25/how-to-code-with-me-flake8.html">Flake8
Hell</a>.</p>

<h3 id="random-code-style-necessities">Random Code Style Necessities</h3>

<p>Every python file must start with the file encoding, a newline, then the module
docstring like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># -*- coding: utf-8 -*-
</span>
<span class="s">"""The module-level docstring."""</span>
</code></pre></div></div>

<p>This docstring has to follow <code class="language-plaintext highlighter-rouge">flake8</code> rules, meaning there’s a short description
that fits on the first line then there’s a period. After that, there can be a
blank line before any other restructured text-formatted documentation you’d
like.</p>

<h2 id="bonus-round-how-to-code-with-ben-gyori">Bonus Round: How to Code with Ben Gyori</h2>

<p>This isn’t something I do, but to maintain a clean git history,
<a href="https://github.com/bgyori">Ben Gyori</a> frequently reminds me to rebase on
master. This keeps a more linear history of what happened and when. Here are his
instructions:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git fetch <span class="nt">--all</span>
<span class="c"># This is your master</span>
git checkout master
<span class="c"># This is their master</span>
git merge <span class="nt">--ff-only</span> upstream/master
git rebase master &lt;your branch name&gt;
<span class="c"># Optionally</span>
git push <span class="nt">-f</span> origin &lt;your branch name&gt;
</code></pre></div></div>

<hr />

<p>This is by no means everything I have to say on this topic. I’ll be back with
more on documentation, ReadTheDocs, using CI, checking unit test coverage, and
more.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="code with me" /><summary type="html"><![CDATA[This blog post is the next installment in the series about all of the very particular ways I do software development in Python. This round is about where to put your code, your tests, your CLI, and the right metadata for each.]]></summary></entry><entry><title type="html">A Reading List of Academic Articles using the Biological Expression Language (BEL)</title><link href="https://cthoyt.com/2020/05/22/bel-papers.html" rel="alternate" type="text/html" title="A Reading List of Academic Articles using the Biological Expression Language (BEL)" /><published>2020-05-22T13:50:00+00:00</published><updated>2020-05-22T13:50:00+00:00</updated><id>https://cthoyt.com/2020/05/22/bel-papers</id><content type="html" xml:base="https://cthoyt.com/2020/05/22/bel-papers.html"><![CDATA[<p>This post is evolving from a reading list to a review of the academic papers
published that are either about or use the
<a href="https://biological-expression-language.github.io">Biological Expression Language (BEL)</a>.
It’s divided into the categories of software/visualization tools,
algorithms/analytical frameworks, data integration, natural language processing,
curation workflows, and downstream applications.</p>

<p>In my <a href="/2020/04/28/history-of-bel.html">history of BEL</a>, I imposed some
quality and impact standards. Without getting into a debate about how peer
review is broken because it’s missing these standards, this list is more
inclusive to papers whether they are likely to be reproducible or useful to the
community, or not.</p>

<p>Some papers in this list were tricky to find - many of the PMI papers do not
mention BEL in the abstract and the journals in which their papers were
published don’t seem to index properly in MEDLINE. You’re welcome to use
<a href="https://pubmed.ncbi.nlm.nih.gov/?term=%22biological+expression+language%22">my search on PubMed</a>
and
<a href="https://europepmc.org/search?query=%22biological%20expression%20language%22">my search on Europe PubMed Central</a>
to see for yourself. I also looked through the publication lists of some key
authors from Selventa and PMI over the last 10 years to find several other
application papers. As always, this list is incomplete due to the lack of
findability/accessibility of papers, my lack of knowledge of <em>everything</em>, and
lack of time to deeply read through all authors’ histories. If you know of a
search tool that might be helpful for improving this list, please let me know.
I’ve been meaning to check out CoCites:</p>

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="en" dir="ltr">Am thrilled this paper is finally out. The most exciting project I&#39;ve ever worked on, a totally new method for searching scientific literature with stunning results.<br /><br />We work to launch the method as a webtool in the coming months. Stay tuned. <a href="https://twitter.com/CoCites?ref_src=twsrc%5Etfw">@cocites</a><br />1/<a href="https://t.co/Ok7RW6yXIn">https://t.co/Ok7RW6yXIn</a> <a href="https://t.co/tkpRqOT6bj">pic.twitter.com/tkpRqOT6bj</a></p>&mdash; Cecile Janssens (@cecilejanssens) <a href="https://twitter.com/cecilejanssens/status/1225789201901215744?ref_src=twsrc%5Etfw">February 7, 2020</a></blockquote>

<p>As always, improvements are welcome. Check my contact info at the bottom of this
post. I’ll be making a major update to this post later to be more like a review
as I’m putting together the new BEL reference paper.</p>

<h2 id="software-and-visualization">Software and Visualization</h2>

<p><a href="https://doi.org/10.1093/bioinformatics/btx660"><strong>PyBEL: a computational framework for Biological Expression Language</strong></a>.
<br /> Hoyt, C. T., Konotopez, A. &amp; Ebeling, C. (2017). <br /> <em>Bioinformatics</em>,
<strong>34</strong>(4), 703–704.</p>

<p><a href="https://doi.org/10.1093/database"><strong>BEL Commons: an environment for exploration and analysis of networks encoded in Biological Expression Language</strong></a>.
<br /> Hoyt, C. T., Domingo-Fernández, D., &amp; Hofmann-Apitius, M. (2018). <br />
<em>Database</em>, <strong>2018</strong>(3), 1–11</p>

<p><a href="https://doi.org/10.1093/bioinformatics/btz289"><strong>INDRA-IPM: Interactive pathway modeling using natural language with automated assembly</strong></a>.
<br /> Todorov, P. V., Gyori, B. M., Bachman, J. A., &amp; Sorger, P. K. (2019).
<br /> <em>Bioinformatics</em>, <strong>35</strong>(21), 4501–4503.</p>

<h2 id="algorithms-and-analytical-frameworks">Algorithms and Analytical Frameworks</h2>

<p><a href="https://doi.org/10.1016/j.drudis.2011.11.008"><strong>A network-based approach to quantifying the impact of biologically active substances</strong></a>.
<br /> Hoeng, J., Deehan, R., Pratt, D., Martin, F., Sewer, A., Thomson, T. M.,
Drubin, D. A., Waters, C. A., de Graaf, D., &amp; Peitsch, M. C. (2012). <br />
<em>Drug Discovery Today</em>, <strong>17</strong>(9), 413–418.</p>

<p><a href="https://doi.org/10.1186/1752-0509-6-54"><strong>Assessment of network perturbation amplitudes by applying high-throughput data to causal biological networks</strong></a>.
<br /> Martin, F., Thomson, T. M., Sewer, A., Drubin, D. a, Mathis, C.,
Weisensee, D., Pratt, D., Hoeng, J., &amp; Peitsch, M. C. (2012). <br /> <em>BMC
Systems Biology</em>, <strong>6</strong>, 54.</p>

<p><a href="https://doi.org/10.1016/j.taap.2013.07.007"><strong>Quantitative assessment of biological impact using transcriptomic data and mechanistic network models</strong></a>.
<br /> Thomson, T. M., Sewer, A., Martin, F., Belcastro, V., Frushour, B. P.,
Gebel, S., Park, J., Schlage, W. K., Talikka, M., Vasilyev, D. M., Westra, J.
W., Hoeng, J., &amp; Peitsch, M. C. (2013). <br /> <em>Toxicology and Applied
Pharmacology</em>, <strong>272</strong>(3), 863–878.</p>

<p><a href="https://doi.org/10.1186/1471-2105-14-340"><strong>Reverse causal reasoning: applying qualitative causal knowledge to the interpretation of high-throughput data</strong></a>.
<br /> Catlett, N. L., Bargnesi, A. J., Ungerer, S., Seagaran, T., Ladd, W.,
Elliston, K. O., &amp; Pratt, D. (2013). <br /> <em>BMC Bioinformatics</em>,
<strong>14</strong>(1), 340.</p>

<p><a href="https://doi.org/10.1186/1756-0500-7-516"><strong>An algorithm for score aggregation over causal biological networks based on random walk sampling</strong></a>.
<br /> Vasilyev, D. M., Thomson, T. M., Frushour, B. P., Martin, F., &amp; Sewer, A.
(2014). <br /> <em>BMC Research Notes</em>, <strong>7</strong>, 516.</p>

<p><a href="https://doi.org/10.1186/1471-2105-15-238"><strong>Quantification of biological network perturbations for mechanistic insight and diagnostics using two-layer causal models</strong></a>.
<br /> Martin, F., Sewer, A., Talikka, M., Xiang, Y., Hoeng, J., &amp; Peitsch, M.
C. (2014). <br /> <em>BMC Bioinformatics</em>, <strong>15</strong>(1), 238.</p>

<p><a href="https://doi.org/10.1093/bioinformatics/btx399"><strong>Multimodal mechanistic signatures for neurodegenerative diseases (NeuroMMSig): a web server for mechanism enrichment</strong></a>.
<br /> Domingo-Fernández, D., Kodamullil, A. T., Iyappan, A., Naz, M., Emon, M.
A., Raschka, T., Karki, R., Springstubbe, S., Ebeling, C., &amp; Hofmann-Apitius, M.
(2017). <br /> <em>Bioinformatics</em>, <strong>33</strong>(22), 3679–3681.</p>

<p><a href="https://doi.org/10.1093/bioinformatics/bty107"><strong>BEL2ABM: Agent-based simulation of static models in Biological Expression Language</strong></a>.
<br /> Gündel, M., Hoyt, C. T., &amp; Hofmann-Apitius, M. (2018). <br />
<em>Bioinformatics</em>, <strong>34</strong>(13), 2316–2318.</p>

<p><a href="https://doi.org/10.1093/bioinformatics/btz117"><strong>BioKEEN: a library for learning and evaluating biological knowledge graph embeddings</strong></a>.
<br /> Ali, M., Hoyt, C. T., Domingo-Fernández, D., Lehmann, J., &amp; Jabeen, H.
(2019). <br /> <em>Bioinformatics</em>, <strong>35</strong>(18), 3538–3540.</p>

<p><a href="https://doi.org/10.1186/s12859-019-3101-1"><strong>Quantifying mechanisms in neurodegenerative diseases using candidate mechanism perturbation amplitude algorithm</strong></a>.
<br /> Karki, R., Kodamullil, A. T., Hoyt, C. T., &amp; Hofmann-Apitius, M. (2019).
<br /> <em>BMC Bioinformatics</em>, <strong>20</strong>(1), 494.</p>

<p><a href="https://doi.org/10.1186/s12859-019-3016-x"><strong>NPA: an R package for computing network perturbation amplitudes using gene expression data and two-layer networks.</strong></a>.
<br /> Martin, F., Gubian, S., Talikka, M., Hoeng, J., &amp; Peitsch, M. C. (2019).
<br /> <em>BMC Bioinformatics</em>, <strong>20</strong>(1), 451.</p>

<p><a href="https://doi.org/10.1109/TBDATA.2021.3050680"><strong>Leveraging Structured Biological Knowledge for Counterfactual Inference: a Case Study of Viral Pathogenesis</strong></a>
<br /> Zucker, J., Paneri, K., Mohammad-Taheri, S., Bhargava, S., Kolambkar, P.,
Bakker, C., Teuton, J., Hoyt, C. T., Oxford, K., Ness, R., &amp; Vitek, O. (2021).
<br /> <em>IEEE Transactions on Big Data</em>, 2021.3050680</p>

<h2 id="data-integration">Data Integration</h2>

<p><a href="https://doi.org/10.3233/JAD-160222"><strong>Using Drugs as Molecular Probes: A Computational Chemical Biology Approach in Neurodegenerative Diseases</strong></a>.
<br /> Emon, M. A. E. K., Kodamullil, A. T., Karki, R., Younesi, E., &amp;
Hofmann-Apitius, M. (2017). <br /> <em>Journal of Alzheimer’s Disease</em>, <strong>56</strong>(2),
677–686.</p>

<p><a href="https://doi.org/10.1038/s41540-018-0078-8"><strong>ComPath: an ecosystem for exploring, analyzing, and curating mappings across pathway databases</strong></a>.
<br /> Domingo-Fernandez, D., Hoyt, C. T., Alvarez, C. B., Marin-Llao, J., &amp;
Hofmann-Apitius, M. (2018). <br /> <em>npj Systems Biology and Applications</em>,
<strong>5</strong>(1), 3.</p>

<p><a href="https://doi.org/10.1101/631812"><strong>Integration of Structured Biological Data Sources using Biological Expression Language</strong></a>.
<br /> Hoyt, C. T., Domingo-Fernández, D., Mubeen, S., Llaó, J. M., Konotopez,
A., Ebeling, C., Birkenbihl, C., Muslu, Ö., English, B., Müller, S., de Lacerda,
M. P., Ali, M., Colby, S., Türei, D., Palacio-Escat, N., &amp; Hofmann-Apitius, M.
(2019). <br /> <em>bioRxiv</em>, 631812.</p>

<p><a href="https://doi.org/10.1186/s12859-019-2863-9"><strong>PathMe: merging and exploring mechanistic pathway knowledge</strong></a>.
<br /> Domingo-Fernández, D., Mubeen, S., Marín-Llaó, J., Hoyt, C. T., &amp;
Hofmann-Apitius, M. (2019). <br /> <em>BMC Bioinformatics</em>, <strong>20</strong>(1), 243.</p>

<p><a href="https://doi.org/10.3389/fgene.2019.01203"><strong>The Impact of Pathway Database Choice on Statistical Enrichment Analysis and Predictive Modeling</strong></a>.
<br /> Mubeen, S., Hoyt, C. T., Gemünd, A., Hofmann-Apitius, M., Fröhlich, H., &amp;
Domingo-Fernández, D. (2019). <br /> <em>Frontiers in Genetics</em>, <strong>10</strong>, 654442.</p>

<h2 id="natural-language-processing">Natural Language Processing</h2>

<p><a href="https://doi.org/10.1093/database/bav057"><strong>Construction of biological networks from unstructured information based on a semi-automated curation workflow</strong></a>.
<br /> Szostak, J., Ansari, S., Madan, S., Fluck, J., Talikka, M., Iskandar, A.,
De Leon, H., Hofmann-Apitius, M., Peitsch, M. C., &amp; Hoeng, J. (2015). <br />
<em>Database</em>, <strong>2015</strong>, bav057.</p>

<p><a href="https://doi.org/10.1093/database/baw113"><strong>Training and evaluation corpora for the extraction of causal relationships encoded in biological expression language (BEL)</strong></a>.
<br /> Fluck, J., Madan, S., Ansari, S., Kodamullil, A. T., Karki, R.,
Rastegar-Mojarad, M., Catlett, N. L., Hayes, W., Szostak, J., Hoeng, J., &amp;
Peitsch, M. (2016). <br /> <em>Database</em>, <strong>2016</strong>, 1–20.</p>

<p><a href="https://doi.org/10.1093/database/baw067"><strong>BioCreative V track 4: a shared task for the extraction of causal network information using the Biological Expression Language</strong></a>.
<br /> Rinaldi, F., Ellendorff, T. R., Madan, S., Clematide, S., van der Lek,
A., Mevissen, T., &amp; Fluck, J. (2016). <br /> <em>Database</em>, <strong>2016</strong>, baw067.</p>

<p><a href="https://doi.org/10.1093/database/baw076"><strong>Coreference resolution improves extraction of Biological Expression Language statements from texts</strong></a>.
<br /> Choi, M., Liu, H., Baumgartner, W., Zobel, J., &amp; Verspoor, K. (2016).
<br /> <em>Database</em>, <strong>2016</strong>, 1–14.</p>

<p><a href="https://doi.org/10.1093/database/baw136"><strong>The BEL information extraction workflow (BELIEF): evaluation in the BioCreative V BEL and IAT track</strong></a>.
<br /> Madan, S., Hodapp, S., Senger, P., Ansari, S., Szostak, J., Hoeng, J.,
Peitsch, M., &amp; Fluck, J. (2016). <br /> <em>Database</em>, <strong>2016</strong>(September), 1–17.</p>

<p><a href="https://doi.org/10.1093/database/baw064"><strong>BelSmile: a biomedical semantic role labeling approach for extracting biological expression language from text</strong></a>.
<br /> Lai, P. T., Lo, Y. Y., Huang, M. S., Hsiao, Y. C., &amp; Tsai, R. T. H.
(2016). <br /> <em>Database</em>, <strong>2016</strong>(June), 1–9.</p>

<p><a href="https://doi.org/10.1093/database/baw079"><strong>BELTracker: evidence sentence retrieval for BEL statements</strong></a>.
<br /> Rastegar-Mojarad, M., Komandur Elayavilli, R., &amp; Liu, H. (2016). <br />
<em>Database</em>, <strong>2016</strong>(June), 1–11.</p>

<p><a href="https://doi.org/10.1093/database/baw156"><strong>BELMiner: Adapting a rule-based relation extraction system to extract biological expression language statements from bio-medical literature evidence sentences</strong></a>.
<br /> Ravikumar, K. E., Rastegar-Mojarad, M., &amp; Liu, H. (2017). <br />
<em>Database</em>, <strong>2017</strong>(1), 1–12.</p>

<p><a href="http://publica.fraunhofer.de/eprints/urn_nbn_de_0011-n-4972978.pdf"><strong>Automatic Extraction of BEL-Statements based on Neural Networks</strong></a>.
<br /> Ali, M., Madan, S., Fischer, A., Petzka, H., &amp; Fluck, J. (2017). <br />
<em>Proceedings of BioCreative VI Challenge and Workshop</em>, October.</p>

<p><a href="https://doi.org/10.15252/msb.20177651"><strong>From word models to executable models of signaling networks using automated assembly</strong></a>.
<br /> Gyori, B. M., Bachman, J. A., Subramanian, K., Muhlich, J. L., Galescu,
L., &amp; Sorger, P. K. (2017). <br /> <em>Molecular Systems Biology</em>, <strong>13</strong>(11), 954.</p>

<p><a href="https://doi.org/10.1093/database/baz084"><strong>The extraction of complex relationships and their conversion to biological expression language (BEL) overview of the BioCreative VI (2017) BEL track</strong></a>.
<br /> Madan, S., Szostak, J., Komandur Elayavilli, R., Tsai, R. T. H., Ali, M.,
Qian, L., Rastegar-Mojarad, M., Hoeng, J., &amp; Fluck, J. (2019). <br />
<em>Database</em>, <strong>2019</strong>(1), 1–17.</p>

<p><a href="https://doi.org/10.1093/database/bay133"><strong>Combining relation extraction with function detection for BEL statement extraction</strong></a>.
<br /> Liu, S., Cheng, W., Qian, L., &amp; Zhou, G. (2019). <br /> <em>Database</em>,
<strong>2019</strong>(4), 1–12.</p>

<p><a href="https://doi.org/10.1186/s12911-019-0758-3"><strong>Hierarchical sequence labeling for extracting BEL statements from biomedical literature</strong></a>.
<br /> Liu, S., Shao, Y., Qian, L., &amp; Zhou, G. (2019). <br /> <em>BMC Medical
Informatics and Decision Making</em>, <strong>19</strong>(Suppl 2).</p>

<p><a href="https://doi.org/10.1093/database/baab005"><strong>Extraction of causal relations based on SBEL and BERT model</strong></a>
<br /> Shao, Y., Li, H., Gu, J., Qian, L., &amp; Zhou, G. (2021). <br /> <em>Database</em>,
<strong>2021</strong>.</p>

<h2 id="curation-workflows">Curation Workflows</h2>

<p><a href="https://doi.org/10.4137/BBI.S12932"><strong>On crowd-verification of biological networks</strong></a>.
<br /> Ansari, S., Binder, J., Boue, S., Di Fabio, A., Hayes, W., Hoeng, J.,
Iskandar, A., Kleiman, R., Norel, R., O’Neel, B., Peitsch, M. C., Poussin, C.,
Pratt, D., Rhrissorrakrai, K., Schlage, W. K., Stolovitzky, G., &amp; Talikka, M.
(2013). <br /> <em>Bioinformatics and Biology Insights</em>, <strong>7</strong>, 307–325.</p>

<p><a href="https://doi.org/10.1007/978-1-4939-7027-8_2"><strong>sbv IMPROVER: Modern Approach to Systems Biology BT - Biological Networks and Pathway Analysis</strong></a>.
<br /> Guryanova, S., &amp; Guryanova, A. (2017). <br /> <em>T. V Tatarinova &amp; Y.
Nikolsky (eds.)</em>, pp. 21–29. Springer New York.</p>

<p><a href="https://doi.org/10.1093/database/baz068"><strong>Re-curation and rational enrichment of knowledge graphs in Biological Expression Language</strong></a>.
<br /> Hoyt, C. T., Domingo-Fernández, D., Aldisi, R., Xu, L., Kolpeja, K.,
Spalek, S., Wollert, E., Bachman, J., Gyori, B. M., Greene, P., &amp;
Hofmann-Apitius, M. (2019). <br /> <em>Database</em>, <strong>2019</strong>(1).</p>

<h2 id="content-and-applications">Content and Applications</h2>

<p><a href="https://doi.org/10.1186/1752-0509-5-168"><strong>A computable cellular stress network model for non-diseased pulmonary and cardiovascular tissue</strong></a>.
<br /> Schlage, W. K., Westra, J. W., Gebel, S., Catlett, N. L., Mathis, C.,
Frushour, B. P., Hengstermann, A., Van Hooser, A., Poussin, C., &amp; Wong, B.
(2011). <br /> <em>BMC Syst Biol</em>, <strong>5</strong>.</p>

<p><a href="https://doi.org/10.1186/1752-0509-5-105"><strong>Construction of a Computable Cell Proliferation Network Focused on Non-Diseased Lung Cells</strong></a>.
<br /> Westra, J. W., Schlage, W. K., Frushour, B. P., Gebel, S., Catlett, N.
L., Han, W., Eddy, S. F., Hengstermann, A., Matthews, A. L., &amp; Mathis, C.
(2011). <br /> <em>BMC Syst Biol</em>, <strong>5</strong>.</p>

<p><a href="https://doi.org/10.4137/BBI.S11154"><strong>Construction of a computable network model for DNA damage, autophagy, cell death, and senescence</strong></a>.
<br /> Gebel, S., Lichtner, R. B., Frushour, B., Schlage, W. K., Hoang, V.,
Talikka, M., Hengstermann, A., Mathis, C., Veljkovic, E., Peck, M., Peitsch, M.
C., Deehan, R., Hoeng, J., &amp; Westra, J. W. (2013). <br /> <em>Bioinformatics and
Biology Insights</em>, <strong>7</strong>, 97–117.</p>

<p><a href="https://doi.org/10.4137/BBI.S11509"><strong>A modular cell-type focused inflammatory process network model for non-diseased pulmonary tissue</strong></a>.
<br /> Westra, J. W., Schlage, W. K., Hengstermann, A., Gebel, S., Mathis, C.,
Thomson, T., Wong, B., Hoang, V., Veljkovic, E., Peck, M., Lichtner, R. B.,
Weisensee, D., Talikka, M., Deehan, R., Hoeng, J., &amp; Peitsch, M. C. (2013).
<br /> <em>Bioinformatics and Biology Insights</em>, <strong>7</strong>, 167–192.</p>

<p><a href="https://doi.org/10.4137/BBI.S12167"><strong>Systematic verification of upstream regulators of a computable cellular proliferation network model on non-diseased lung cells using a dedicated dataset</strong></a>.
<br /> Belcastro, V., Poussin, C., Gebel, S., Mathis, C., Schlage, W. K.,
Lichtner, R. B., Quadt-Humme, S., Wagner, S., Hoeng, J., &amp; Peitsch, M. C.
(2013). <br /> <em>Bioinformatics and Biology Insights</em>, <strong>7</strong>, 217–230.</p>

<p><a href="https://doi.org/10.1016/j.drudis.2013.07.023"><strong>Case study: the role of mechanistic network models in systems toxicology</strong></a>.
<br /> Hoeng, J., Talikka, M., Martin, F., Sewer, A., Yang, X., Iskandar, A.,
Schlage, W. K., &amp; Peitsch, M. C. (2014). <br /> <em>Drug Discovery Today</em>,
<strong>19</strong>(2), 183–192.</p>

<p><a href="https://doi.org/10.3109/15376516.2014.943441"><strong>In vitro systems toxicology approach to investigate the effects of repeated cigarette smoke exposure on human buccal and gingival organotypic epithelial tissue cultures.</strong></a>.
<br /> Schlage, W. K., Iskandar, A. R., Kostadinova, R., Xiang, Y., Sewer, A.,
Majeed, S., Kuehn, D., Frentzel, S., Talikka, M., Geertz, M., Mathis, C.,
Ivanov, N., Hoeng, J., &amp; Peitsch, M. C. (2014). <br /> Toxicology Mechanisms and
Methods, <strong>24</strong>(7), 470–487.</p>

<p><a href="https://doi.org/10.1186/1479-5876-12-185"><strong>A vascular biology network model focused on inflammatory processes to investigate atherogenesis and plaque instability</strong></a>.
<br /> De León, H., Boué, S., Schlage, W. K., Boukharov, N., Westra, J. W.,
Gebel, S., VanHooser, A., Talikka, M., Fields, R. B., Veljkovic, E., Peck, M.
J., Mathis, C., Hoang, V., Poussin, C., Deehan, R., Stolle, K., Hoeng, J., &amp;
Peitsch, M. C. (2014). <br /> <em>Journal of Translational Medicine</em>, <strong>12</strong>(1).</p>

<p><a href="https://doi.org/10.1186/2041-1480-5-28"><strong>Micropublications: A semantic model for claims, evidence, arguments and annotations in biomedical communications</strong></a>.
<br /> Clark, T., Ciccarese, P. N., &amp; Goble, C. A. (2014). <br /> <em>Journal of
Biomedical Semantics</em>, <strong>5</strong>(1), 1–33.</p>

<p><a href="https://doi.org/10.1155/2015/737168"><strong>Computational Modelling Approaches on Epigenetic Factors in Neurodegenerative and Autoimmune Diseases and Their Mechanistic Analysis</strong></a>.
<br /> Khanam Irin, A., Tom Kodamullil, A., Gündel, M., &amp; Hofmann-Apitius, M.
(2015). <br /> <em>Journal of Immunology Research</em>, <strong>2015</strong>, 1–10.</p>

<p><a href="https://doi.org/10.4137/GRSB.S40031"><strong>Semi-automated curation allows causal network model building for the quantification of age-dependent plaque progression in ApoE−/− mouse</strong></a>.
<br /> Szostak, J., Martin, F., Talikka, M., Peitsch, M. C., &amp; Hoeng, J. (2016).
<br /> <em>Gene Regulation and Systems Biology</em>, <strong>10</strong>, 95–103.</p>

<p><a href="https://doi.org/10.4137/GRSB.S39076"><strong>Community-reviewed biological network models for toxicology and drug discovery applications</strong></a>.
<br /> Namasivayam, A. A., Morales, A. F., Lacave, Á. M. F., Tallam, A.,
Simovic, B., Alfaro, D. G., Bobbili, D. R., Martin, F., Androsova, G.,
Shvydchenko, I., Park, J., Val Calvo, J., Hoeng, J., Peitsch, M. C., Racero, M.
G. V., Biryukov, M., Talikka, M., Pérez, M. B., Rohatgi, N., … Xiang, Y. (2016).
<br /> <em>Gene Regulation and Systems Biology</em>, <strong>10</strong>, 51–66.</p>

<p><a href="https://doi.org/10.1093/bib/bbv063"><strong>Reasoning over genetic variance information in cause-and-effect models of neurodegenerative diseases</strong></a>.
<br /> Naz, M., Kodamullil, A. T., &amp; Hofmann-Apitius, M. (2016). <br />
<em>Briefings in Bioinformatics</em>, <strong>17</strong>(3), 505–516.</p>

<p><a href="https://doi.org/10.3233/JAD-170255"><strong>Of Mice and Men: Comparative Analysis of Neuro-Inflammatory Mechanisms in Human and Mouse Using Cause-and-Effect Models</strong></a>.
<br /> Kodamullil, A. T., Iyappan, A., Karki, R., Madan, S., Younesi, E., &amp;
Hofmann-Apitius, M. (2017). <br /> <em>Journal of Alzheimer’s Disease</em>, <strong>59</strong>(3),
1045–1055.</p>

<p><a href="https://doi.org/10.3233/JAD-170440"><strong>Comorbidity Analysis between Alzheimer’s Disease and Type 2 Diabetes Mellitus (T2DM) Based on Shared Pathways and the Role of T2DM Drugs</strong></a>.
<br /> Karki, R., Kodamullil, A. T., &amp; Hofmann-Apitius, M. (2017). <br />
<em>Journal of Alzheimer’s Disease</em>, <strong>60</strong>(2), 721–731.</p>

<p><a href="https://doi.org/10.1080/17460441.2017.1335302"><strong>Novel approaches to develop community-built biological network models for potential drug discovery</strong></a>.
<br /> Talikka, M., Bukharov, N., Hayes, W. S., Hofmann-Apitius, M.,
Alexopoulos, L., Peitsch, M. C., &amp; Hoeng, J. (2017). <br /> <em>Expert Opinion on
Drug Discovery</em>, <strong>12</strong>(8), 849–857.</p>

<p><a href="https://doi.org/10.1093/database/bay050"><strong>A systematic approach for identifying shared mechanisms in epilepsy and its comorbidities</strong></a>.
<br /> Hoyt, C. T., Domingo-Fernández, D., Balzer, N., Güldenpfennig, A., &amp;
Hofmann-Apitius, M. (2018). <br /> <em>Database</em>, <strong>2018</strong>(1).</p>

<p><a href="https://doi.org/10.3389/fgene.2019.00087"><strong>Construction of a suite of computable biological network models focused on mucociliary clearance in the repiratory tract</strong></a>.
<br /> Yepiskoposyan, H., Talikka, M., Vavassori, S., Martin, F., Sewer, A.,
Gubian, S., Luettich, K., Peitsch, M. C., &amp; Hoeng, J. (2019). <br /> <em>Frontiers
in Genetics</em>, <strong>10</strong>(FEB), 1–12.</p>

<p><a href="https://doi.org/10.1186/s40425-019-0764-0"><strong>34th Annual Meeting &amp; Pre-Conference Programs of the Society for Immunotherapy of Cancer (SITC 2019): part 2 : National Harbor, MD, USA. 10 November 2019</strong></a>.
<br /> (2019). <br /> <em>Journal for Immunotherapy of Cancer</em>, <strong>7</strong>(Suppl
1), 283.</p>

<ul>
  <li>See entry entitled “Reverse causal inferencing on lung adenocarcinoma patients
reveals a stem cell-like molecular subtype associated with pack year history”</li>
</ul>

<p><a href="https://doi.org/10.1038/s41598-020-60175-3"><strong>The reduction of DSS-induced colitis severity in mice exposed to cigarette smoke is linked to immune modulation and microbial shifts.</strong></a>
<br /> Lo Sasso, G., Phillips, B. W., Sewer, A., Battey, J. N. D., Kondylis, A.,
Talikka, M., Titz, B., Guedj, E., Peric, D., Bornand, D., Dulize, R., Merg, C.,
Corciulo, M., Ouadi, S., Yanuar, R., Tung, C. K., Ivanov, N. V, Peitsch, M. C.,
&amp; Hoeng, J. (2020). <br /> <em>Scientific Reports</em>, <strong>10</strong>(1), 3829.</p>

<p><a href="https://doi.org/10.3389/fbioe.2020.00074"><strong>A Computational Approach for Mapping Heme Biology in the Context of Hemolytic Disorders</strong></a>.
<br /> Humayun, F., Domingo-Fernández, D., Paul George, A. A., Hopp, M. T.,
Syllwasschy, B. F., Detzel, M. S., Hoyt, C. T., Hofmann-Apitius, M., &amp; Imhof, D.
(2020). <br /> <em>Frontiers in Bioengineering and Biotechnology</em>, <strong>8</strong>(March),
1–10.</p>

<p><a href="https://doi.org/10.1101/2020.04.14.040667"><strong>COVID-19 Knowledge Graph: a computable, multi-modal, cause-and-effect knowledge model of COVID-19 pathophysiology</strong></a>.
<br /> Domingo-Fernández, D., Baksi, S., Schultz, B., Gadiya, Y., Karki, R.,
Raschka, T., Ebeling, C., Hofmann-Apitius, M., &amp; Kodamullil, A. T. (2020).
<br /> <em>bioRxiv</em>, 2020.04.14.040667.</p>

<p><a href="https://doi.org/10.1101/2020.06.09.142125"><strong>Unravelling the debate on heme effects in COVID-19 infections</strong></a>.
<br /> Hopp, M., Domingo-Fernández, D., Gadiya, Y., Detzel, M. S., Schmalohr, B.
F., Steinbock, F., Imhof, D., &amp; Martin Hofmann-Apitius, M. (2020). <br />
<em>bioRxiv</em>, 2020.06.09.142125</p>

<p><a href="https://doi.org/10.3233/JAD-200752"><strong>Data-Driven Modeling of Knowledge Assemblies in Understanding Comorbidity Between Type 2 Diabetes Mellitus and Alzheimer’s Disease</strong></a>.
<br /> Karki, R., Madan, S., Gadiya, Y., Domingo-Fernández, D., Kodamullil, A.
T., &amp; Hofmann-Apitius, M. (2020). <br /> <em>Journal of Alzheimer’s Disease</em>,
<strong>78</strong>(1), 87–95.</p>

<p><a href="https://doi.org/10.3233/JAD-201397"><strong>A Systems Biology Approach for Hypothesizing the Effect of Genetic Variants on Neuroimaging Features in Alzheimer’s Disease</strong></a>.
<br /> Khatami, S. G., Domingo-Fernández, D., Mubeen, S., Hoyt, C. T., Robinson,
C., Karki, R., Iyappan, A., Kodamullil, A. T., &amp; Hofmann-Apitius, M. (2021).
<br /> <em>Journal of Alzheimer’s Disease</em>, 201397</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="BEL" /><summary type="html"><![CDATA[This post is evolving from a reading list to a review of the academic papers published that are either about or use the Biological Expression Language (BEL). It’s divided into the categories of software/visualization tools, algorithms/analytical frameworks, data integration, natural language processing, curation workflows, and downstream applications.]]></summary></entry><entry><title type="html">The Trouble with Ontologies, or, How to Build an Ontology</title><link href="https://cthoyt.com/2020/05/12/building-an-ontology.html" rel="alternate" type="text/html" title="The Trouble with Ontologies, or, How to Build an Ontology" /><published>2020-05-12T08:00:00+00:00</published><updated>2020-05-12T08:00:00+00:00</updated><id>https://cthoyt.com/2020/05/12/building-an-ontology</id><content type="html" xml:base="https://cthoyt.com/2020/05/12/building-an-ontology.html"><![CDATA[<p>Everyone’s talking about biomedical ontologies! Let’s look at where most people
go wrong and how to do it right.</p>

<h1 id="the-trouble-with-biomedical-ontologies">The Trouble with Biomedical Ontologies</h1>

<p>There’s a lot of confusion within the biomedical community as to what
constitutes an ontology. It’s been aggravated by the impenetrable discourse used
by ontologists similar to the way that mathematicians and computer scientists
have a habit of using obscure mathematical notation to obfuscate their work.
Even worse, this has led to confusion within the biomedical community as to what
an ontology is for.</p>

<p>For example, biocurators find ontologies useful as a research tool for looking
up the definitions of entities using tools like the
<a href="https://www.ebi.ac.uk/ols/index">Ontology Lookup Service (OLS)</a>. Database
maintainers and biocurators often look to ontologies to decide what their data
should look like and enable other people to understand it readily. Similarly,
natural language processing (NLP) researchers find ontologies incredibly useful
in named entity recognition because ontologies often contain preferred labels in
many languages and synonyms for its terms. Software developers and end-users
find ontologies useful for organizing and conveying information in a
hierarchical manner.</p>

<p>While it might be true that ontologies have the ability to support all of those
uses, none accurately portray ontologies. The following table presents more
appropriate vocabulary to describe each of these use cases:</p>

<table>
  <thead>
    <tr>
      <th>Phrase</th>
      <th>Definition</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Semantic Space</td>
      <td>An enumerated set of entities</td>
    </tr>
    <tr>
      <td>Controlled Vocabulary</td>
      <td>An enumerated set of entities and their names</td>
    </tr>
    <tr>
      <td>Dictionary</td>
      <td>An enumerated set of entities and their definitions</td>
    </tr>
    <tr>
      <td>Thesaurus</td>
      <td>An enumerated set of entities and their synonyms</td>
    </tr>
    <tr>
      <td>Taxonomy / Hierarchy</td>
      <td>An enumerated set of entities with one parent for each</td>
    </tr>
    <tr>
      <td>Multi-Hierarchy</td>
      <td>An enumerated set of entities with one or more parents for each</td>
    </tr>
  </tbody>
</table>

<p>Database maintainers need controlled vocabularies to improve the utility of
their databases, researchers can leverage dictionaries to learn the definitions,
NLP developers need thesauri containing synonyms to train their models, and
software developers need hierarchies or multi-hierarchies to support
organization.</p>

<p>The actual purpose of an ontology is to first define the rules for how
information is organized and second to apply those rules and actually store
information. This means that ontologies can define that entities need an
identifier, they need a preferred label, they can have synonyms, they can have
equivalences to other entities in other ontologies, they can have relationships
in the form of triples (like parent-child relationships), and they can even have
relationships in the form of quadruples or higher-dimensional tuples. There are
“upper-level” ontologies that take care of some of the common definitions that
can be shared throughout a domain - you’ve probably noticed that most biomedical
ontologies have this core of previously mentioned relationship types. That
enables “lower-level” ontologies to focus on storing the information. Since most
researchers only interact with the “lower-level” ontologies, it can be
understood where the confusion came from.</p>

<p>In practice, this confusion is mostly harmless. We have numerous high-quality
ontologies covering a huge amount of biology and related fields, as well as the
<a href="http://www.obofoundry.org">OBO Foundry</a> to vet them for quality and host them.
Note: <a href="http://bioportal.bioontology.org/">BioPortal</a> is another place for
hosting, but it seems like it has a much lower (or non-existent?) threshold for
quality.</p>

<p>The danger lies in the edge cases. When there are no high quality biomedical
ontologies in a given area, researchers are often inclined to generate their
own. The rest of this blog post is about what happens next, where it all goes
wrong, and how you can avoid it when you’re in the situation that you just
became an ontologist, too.</p>

<h1 id="how-to-build-your-own-biomedical-ontology">How to Build Your Own Biomedical Ontology</h1>

<p>The curation I trust most is by people who know what they’re doing, and more
importantly by people who love what they’re doing.</p>

<p>Given the choice between curating information about a new protein (e.g. the
proteins in the novel coronavirus) myself or having one of the excellent
curators at <a href="https://www.uniprot.org/">UniProt</a> do it, I would choose UniProt
every time. Given the choice between the world’s leading coronavirus researcher
and a UniProt curator with no previous knowledge about the novel coronavirus, I
would still choose the UniProt curator. The UniProt curators love what they do,
they know how to do it well, and they do it right. Same goes for lots of other
groups that I’ve praised elsewhere in this blog. So keep in mind while you’re
reading this guide that you might be causing more harm than good by making yet
another ontology.</p>

<h2 id="preparation">Preparation</h2>

<p>Before you start curating you need to do a bit of planning.</p>

<h3 id="pick-a-memorable-name-and-prefix">Pick a memorable name and prefix</h3>

<p>There are a lot of ontologies, so pick a name that’s both unique and
descriptive. Then, you need to pick a relatively short “prefix” which will be
the first part of <a href="https://en.wikipedia.org/wiki/CURIE">compact URIs (CURIEs)</a>
that point to entries in your ontology. Many ontologies use an acronym as their
prefix, but make sure you don’t cause a conflict or confusion with a previously
existing one. You can search through registries like
<a href="https://identifiers.org">Identifiers.org</a>, the OLS, or the
<a href="http://www.obofoundry.org/">OBO Foundry</a> (which, on a side note, don’t exactly
contain all the same stuff) to check out the existing landscape.</p>

<h3 id="pick-a-scheme-for-identifiers">Pick a scheme for identifiers</h3>

<p>Even if you’re just here to build and maintain your controlled vocabulary, it’s
still necessary to give identifiers to each of the entries in your ontology. In
practice, each entry’s identifier should conform to the
<a href="https://pubmed.ncbi.nlm.nih.gov/18078503/">Minimal Information Requested in the Annotation of Biochemical Models (MIRIAM)</a>
standard. It states that identifiers should have the following five properties:</p>

<ol>
  <li>Uniqueness</li>
  <li>Perenniality</li>
  <li>Standards-Compliant</li>
  <li>Resolvability</li>
  <li>Free Usability</li>
</ol>

<p>There’s a really important thing to keep in mind - identifiers are not numbers.
Even if they look like them, they’re strings. If that doesn’t make sense to you,
then just think about what it would mean to “add” two identifiers to each other.
It would be nonsense to think of identifiers as numbers because they don’t do
what numbers do. With that in mind, there are a few common identifier schemes:</p>

<ul>
  <li>Identifiers that look like numbers, like PubMed identifiers. An example from
PubMed is <a href="https://identifiers.org/pubmed:29048466">29048466</a>.</li>
  <li>Identifiers that look like numbers, but are a fixed width with left-padded
zeros. An example from the
<a href="https://www.ebi.ac.uk/efo/">Experimental Factor Ontology</a> is
<a href="https://identifiers.org/efo:0004859">0004859</a>.</li>
  <li>Identifiers that look like numbers, but are a fixed width with left-padded
zeros and are prefixed with the prefix itself separated by a colon, so the
identifier itself looks like a CURIE. An example from the
<a href="http://geneontology.org/">Gene Ontology</a> is
<a href="https://identifiers.org/GO:0006915">GO:0006915</a>. This is sort of confusing,
and has been dubbed the GOGO problem. Or the Bananananana problem.</li>
  <li>Identifiers that look like numbers, but are prefixed with part of all of the
prefix. An example from <a href="https://www.ebi.ac.uk/chembl/">ChEMBL</a> is
<a href="https://identifiers.org/chembl.compound:CHEMBL941">CHEMBL941</a> where the
prefix is <code class="language-plaintext highlighter-rouge">chembl.compound</code>.</li>
  <li>Identifiers that have a short letter prefix then a fixed width number with
left-padded zeros. An example from MeSH is
<a href="https://identifiers.org/mesh:D013313">D013313</a>.</li>
</ul>

<p>My favorite is the MeSH style, because it allows for the most information to be
conveyed succinctly. You should use numbers of width 6 or 7, even if you only
plan on curating a few dozen or a hundred terms.</p>

<p>Please don’t use the GO style identifiers, because this creates a ton of
confusion.</p>

<p>You should also write down what the regular expression that goes with your
identifiers for later validation. In the MeSH example, the regular expression is
<code class="language-plaintext highlighter-rouge">^(C|D)\d{6,9}$</code>, which means it either starts with C or D, and is followed by
between 6 and 9 numbers. The <code class="language-plaintext highlighter-rouge">^</code> means beginning of the string and <code class="language-plaintext highlighter-rouge">$</code> means end
of the string, so it’s clear that nothing can precede or follow.</p>

<h3 id="pick-your-scope">Pick your scope</h3>

<p>The last, and most important, part of planning is to pick the scope of your
ontology. You have to choose what kinds of entities you want to include (genes,
proteins, side effects, etc.). Keep in mind that if you’re picking one of these
examples, there’s probably already a good nomenclature source for it, so it’s
best you don’t curate it again.</p>

<h2 id="curation">Curation</h2>

<p>It’s time to start curating entries in your ontology. Most people go right to
<a href="https://protege.stanford.edu/">Protégé</a>.</p>

<p><em>Don’t</em>.</p>

<p>Protégé is a perfect way to get bogged down in the ivory tower that is ontology.
Instead, it’s better to focus on the aspects of the ontology that I think are
practically the most important. So in this guide, we’re going to use a set of
interconnected tab-separated values (TSV) documents. Why TSV? Because
comma-separated values (CSV) documents look awful and Excel sheets can’t be
diff’d / viewed in GitHub. However, I would accept the following alternative:</p>

<blockquote class="twitter-tweet">
<p lang="en" dir="ltr">I propose we change CSV to Clown Separated Values since that tends to be about how well CSV parsers work. <a href="https://t.co/C8Uys3H9Kp">pic.twitter.com/C8Uys3H9Kp</a></p>&mdash; Kevin Jones 🏳️‍🌈 🧼👏 (@vcsjones) <a href="https://twitter.com/vcsjones/status/1170828523881271298?ref_src=twsrc%5Etfw">September 8, 2019</a></blockquote>

<p>In a later post, I’ll come back to how to programatically generate OWL, OBO,
BEL, and other formats that are commonly used for ontologies from your curation
sheets.</p>

<h3 id="curate-entities">Curate entities</h3>

<p>The most important thing in an ontology is the entities. Make a file called
<em>entities.tsv</em>. It needs a few columns to hold the most important information
for each entity:</p>

<ol>
  <li>Identifier - the identifier of the entity</li>
  <li>Name - the preferred name of the entity in the main language of the ontology</li>
  <li>Type - the entity type. For example, the Gene Ontology has three entity
types - biological process, cellular component, and molecular function (they
call them “namespaces”). This isn’t the same as the parent of the entity</li>
  <li>References - keep a comma-separated list of CURIEs pointing to resources that
have more information about this entity from PubMed, PubMed Central, etc.</li>
  <li>Description - a short description of the entity written as prose. Shamelessly
borrow from Wikipedia if appropriate, but remember to cite your source!</li>
  <li>Curator <a href="https://orcid.org/">ORCID</a> identifier - it’s really important to
keep track of who added entries to the ontology so you can get in touch when
there is confusion or errors are found. The ORCID identifier is the best
unambiguous way to do this.</li>
</ol>

<p>You should write and enforce a style guide (e.g., only proper nouns are
capitalized in labels for entities) for names and definitions while you’re here.</p>

<p>If there are other pieces of information that all entities must have in your
ontology, then you can also include it in this sheet. Later, the properties and
relationships sheets can be used for other information and other relationships
such as parent/child relationships, physical properties, etc..</p>

<h3 id="curate-synonyms">Curate synonyms</h3>

<p>Make a file called <em>synonyms.tsv</em>. It needs a few columns to describe synonyms
for each term and the provenance of where they came from:</p>

<ol>
  <li>Identifier - the identifier of the entity that matches to the <em>entities.tsv</em>
sheet</li>
  <li>Synonym - the actual text you found</li>
  <li>Provenance - a CURIE describing the source that had the synonym. This might
be a PubMed, PubMed Central, URL, or related.</li>
  <li>Synonym Semantics - is this an exact synonym, a broad synonym, a narrow
synonym, or a related synonym? Each entry should only be one of EXACT,
NARROW, BROAD, or RELATED as defined in the
<a href="https://owlcollab.github.io/oboformat/doc/GO.format.obo-1_4.html">OBO 1.4 standard</a>.
If you’re not sure, just put EXACT.</li>
</ol>

<h3 id="curate-xrefs">Curate xrefs</h3>

<p>Make a file called <em>xrefs.tsv</em>. It needs three columns:</p>

<ol>
  <li>Identifier - the identifier of the entity that matches to the <em>entities.tsv</em>
sheet</li>
  <li>Xref Prefix - the prefix for the data source that describes the same entity</li>
  <li>Xref Identifier - the local identifier of the entity in the xref’s data
source</li>
</ol>

<p>The prefix and identifier for the prefix are split to avoid the headache of
parsing CURIEs later.</p>

<p>It’s best to consider xrefs as equivalences. All other relationships should be
in the relationships page (later).</p>

<h3 id="curate-typedefs">Curate typedefs</h3>

<p>An xref is a very specific type of relationship, so it has first-class status.
The parent-child relationship is also first-class and it goes without saying.
The rest of the relationships (that can be expressed realized as triples) need
to be either pulled in from a previously existing ontology like the
<a href="http://www.obofoundry.org/ontology/ro.html">Relation Ontology</a> or
<a href="https://www.ebi.ac.uk/ols/ontologies/mi">PSI-MI</a>, or defined in a structured
way. Make a file called <em>typedefs.tsv</em>, to borrow from the OBO nomenclature for
defining relationships. It should have the following columns:</p>

<ol>
  <li>Prefix - could be the same as the current ontology, or an external one</li>
  <li>Identifier - if it’s from the current ontology, you might consider using a
different identifier scheme than for entities. For example, WikiData uses
<code class="language-plaintext highlighter-rouge">^Q\d+$</code> for entities and <code class="language-plaintext highlighter-rouge">^P\d+$</code> for relationships</li>
  <li>Name - The preferred name of the relationship</li>
  <li>(Optional) Inverse Of - If the relationship can be defined as the inverse of
another, put its CURIE here</li>
  <li>Parent(s) - a comma-separated list of the relation’s parents’ CURIEs. This is
a special case that doesn’t appear in the relationships sheets because <em>isA</em>
relationships are so important.</li>
</ol>

<h3 id="curate-relationships">Curate relationships</h3>

<p>You’re ready to use the relationships defined in <em>typedefs.tsv</em> to write out
relationships. Make <em>out_relations.tsv</em> with the following columns:</p>

<ol>
  <li>Identifier - the identifier of the entity that matches to the <em>entities.tsv</em>
sheet</li>
  <li>Relation Prefix</li>
  <li>Relation Identifier</li>
  <li>Target Prefix</li>
  <li>Target Identifier</li>
  <li>Target Name (Optional, but useful for readers)</li>
</ol>

<p>Similarly, make another sheet called <em>in_relations.tsv</em> with the following
columns:</p>

<ol>
  <li>Source Prefix</li>
  <li>Source Identifier</li>
  <li>Source Name (Optional, but useful for readers)</li>
  <li>Relation Prefix</li>
  <li>Relation Identifier</li>
  <li>Identifier - the identifier of the entity that matches to the <em>entities.tsv</em>
sheet</li>
</ol>

<p>Between these two sheets, you can encode relationships between entities in the
ontology that are both incoming and outgoing, removing the need to define ad-hoc
inverses of common relationships, like <em>isA</em>.</p>

<h3 id="curate-properties">Curate properties</h3>

<p>Properties are like relationships that point to scalar values instead of other
entities. For a counterexample, synonyms are a first-class property that
contains lots of extra metadata and therefore get their own sheet.</p>

<p>The rest of the properties will appear here. A good example of a property is the
chemical formula, SMILES string, and mass of a given small molecule in the
<a href="https://www.ebi.ac.uk/chebi/">ChEBI ontology</a>. However, not all entries in the
ChEBI ontology are small molecules, so if they were following this guide, it
might not have made sense to put that property in the <em>entities.tsv</em> sheet. Make
a sheet called <em>properties.tsv</em> with the following columns:</p>

<ol>
  <li>Identifier - the identifier of the entity that matches to the <em>entities.tsv</em>
sheet</li>
  <li>Property - it’s up to you how to decide what the properties in your ontology
are. It’s not as common to define it as precisely as with relationships</li>
  <li>Value</li>
  <li>(Optional) Data Type - the XSD data type of the value for the property. If
this isn’t important to you, your life will probably be better by leaving it
out</li>
</ol>

<p>Now that you’ve made all the sheets, you can make sure that your curators do
their best job to fill out entries in each of them every time a new entity is
added. It’s also necessary to keep track of the uniqueness of entity identifiers
as new ones are added. It’s best if they’re consecutive and increasing, too.</p>

<h2 id="maintenance">Maintenance</h2>

<p>One of the other real dangers of starting your own ontology is the entire
concept of maintenance and quality assurance. If you’re working in an academic
group, it’s highly unlikely that you will have the resources, motivation, or
willpower to maintain the ontology that you are building. This can be proven by
reading through the ontologies listed in BioPortal. While this might be
unavoidable, there are a few things that you can do before your time as a PhD
student, Postdoc, or whatever comes to an end to make sure that your ontology is
actually useful for somebody else.</p>

<p>I’ve already given my explanation of why to use TSV - it makes sure there’s no
conflicts with spaces or commas, tabs never show up in real text, and GitHub
will make nice renders of TSVs and show you the diffs as versions change, versus
Excel documents, which are saved as binary.</p>

<h3 id="version-control">Version Control</h3>

<p>As I’ve just alluded, use version control. Keep track of how your ontology
changes over time by making a repository on GitHub. I’ve heard rumors that git
was created by Linus Torvalds to slow people down, so you should use an
interactive GUI for git like <a href="https://desktop.github.com/">GitHub Desktop</a>
(you’re not a martyr!). While you’re working on GitHub, you should use the
<a href="https://guides.github.com/introduction/flow/">GitHub Flow workflow</a>, which
involves forking (or branching), making pull requests, then reviewing and
discussing before merging into master. This is more relevant for people who are
working on teams. If you’re not working on a team, try pulling in a collaborator
to review your work as a pull request. Or email/tweet me! I’d be happy to help
if you’re working in open source with a publicly usable license.</p>

<h3 id="tooling">Tooling</h3>

<p>The next few suggestions rely on a bit of technical expertise. The first is that
you should write scripts that validate the content’s integrity, formatting,
correctness, or whatever rules you can come up with. Then you should use
continuous integration (e.g., <a href="https://travis-ci.com/">Travis-CI</a> or
<a href="https://github.com/features/actions">GitHub Actions</a>) to run those scripts on
every commit to give feedback. If you’re working using the GitHub Flow fork/pull
request workflow, then you can always ask your curators to make sure that their
content doesn’t make the validation scripts fail before merging them into
master.</p>

<p>Next, you should write scripts that export all of your content into common
formats so others can consume it like OWL, OBO, BEL Namespace, etc.
Additionally, it’s nice to automatically build a website that displays all the
curated content and allows people to explore it. GitHub will even host the site
for free.</p>

<p>These suggestions probably sound a bit abstract or scary if you’re not a
seasoned programmer, so in a later post, I’ll provide you with a
<a href="https://github.com/cookiecutter/cookiecutter">cookiecutter</a> template repository
with all the files, scripts, and configuration that you need to do this without
any programming at all. An example of most of it in practice is the Curation of
Neurodegeneration Supporting Ontology (CONSO)
(<a href="https://github.com/pharmacome/conso">source code</a>;
<a href="https://pharmacome.github.io/conso/">web site</a>). It has a few differences from
the recommendations I’ve made in this post - some of them inspired by choices I
made during the curation of CONSO that I think could have been done better.</p>

<h3 id="choose-a-license">Choose a License</h3>

<p>The license tells other people how they’re allowed to use your ontology. If you
don’t use an appropriate open license, other people will not be legally allowed
to use your ontology. And if that’s the case, there really wasn’t a point to
making it (yes, I’m being pedantic here). Check out
<a href="https://choosealicense.com/non-software/">https://choosealicense.com/non-software/</a>
for some pointers. I suggest the
<a href="https://choosealicense.com/licenses/cc0-1.0/">CC0</a> license, which is the most
usable one out there. Don’t fear - people will cite your work and thank you for
it, even if the license doesn’t legally obligate them to.</p>

<h3 id="making-releases-and-long-term-maintenance">Making Releases and Long Term Maintenance</h3>

<p>If you’re using GitHub, you can easily integrate the repository with
<a href="https://zenodo.org/">Zenodo</a>, which archives the repository when you make a tag
and assigns a digital object identifier (DOI) to each release. You might also
want to make releases to the OBO Foundry or BioPortal. You might also want to
register your prefix at Identifiers.org to give your CURIEs maximum legitimacy.</p>

<p>Even with the best intentions, you will inevitably have to change some names
over time. This is okay because your identifiers are persistent! However, you
might have to retire entries. This might mean adding a column to <em>entities.tsv</em>
with the date that a term is made obsolete.</p>

<h3 id="upgrade-to-more-powerful-tooling">Upgrade to more powerful tooling</h3>

<p>While this guide was focused on how to get started with building ontologies if
you’re new, there are obviously a lot of good reasons why people use more
powerful formalisms like OWL to curate their ontologies. Chris Mungall, a
prominent ontologist, gave me some feedback via Twitter that’s definitely worth
a ready from anyone who made it this far in the following thread:</p>

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="en" dir="ltr">I really like Charlie&#39;s take here, even though some of it will annoy many ontologists and my <a href="https://twitter.com/OBOFoundry?ref_src=twsrc%5Etfw">@OBOFoundry</a> colleagues! Lots of sensible advice, would make a good PLOS 10 rules article. I do have a few suggestions and comments... 1/ <a href="https://t.co/LXxa13LJ4a">https://t.co/LXxa13LJ4a</a></p>&mdash; Chris Mungall (@chrismungall) <a href="https://twitter.com/chrismungall/status/1260394048839225344?ref_src=twsrc%5Etfw">May 13, 2020</a></blockquote>

<p>To summarize, he linked to the following article to show how OWL enables the
Gene Ontology to do many more powerful things, such as axiomization, that just
don’t fit into a TSV-only world for curation.</p>

<blockquote>
  <p><a href="https://doi.org/10.1101/010090">Use of OWL within the Gene Ontology</a>
Christopher J Mungall, Heiko Dietze, David Osumi-Sutherland. <em>bioRxiv</em>,
010090; DOI: <code class="language-plaintext highlighter-rouge">10.1101/010090 </code></p>
</blockquote>

<p>He also gave two links to some previously developed tools that can help you get
started with generating ontologies from tables:</p>

<ul>
  <li>https://github.com/INCATools/ontology-development-kit/</li>
  <li>http://robot.obolibrary.org/template</li>
</ul>

<p>These accomplish a similar goal to what I’ll present in the next blog post, and
I’ll have to do my due diligence to figure out how they work to provide a more
in-depth comparison.</p>

<hr />

<p>You can’t compete with UniProt, the Disease Ontology, the Gene Ontology, or
other groups that exist to maintain high quality resources.</p>

<p>So don’t.</p>

<p>Join them.</p>

<p>Make sure that your ontology is written well so the relevant parts can be
incorporated into these and other high quality, maintained ontologies. Then, get
in touch with their maintainers. Tweet at them, send GitHub issues, etc. They’ll
be happy to get input on what they should do next, because, like I said before,
these people love what they do. And there’s nothing better than seeing that
something you are proud of is useful for other people.</p>

<p>Stay tuned for my next post where I’ll give you the code I wrote to do all the
things I recommended before. I’ll put my money where my mouth is and present my
ontology that led to building this curation environment and ultimately writing
this post - the
<a href="https://github.com/pharmacome/conso">Curation of Neurodegeneration Supporting Ontology</a>.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="ontology" /><category term="curation" /><summary type="html"><![CDATA[Everyone’s talking about biomedical ontologies! Let’s look at where most people go wrong and how to do it right.]]></summary></entry><entry><title type="html">A Listing of Publicly Available Content in the Biological Expression Language (BEL)</title><link href="https://cthoyt.com/2020/04/30/public-bel-content.html" rel="alternate" type="text/html" title="A Listing of Publicly Available Content in the Biological Expression Language (BEL)" /><published>2020-04-30T16:50:00+00:00</published><updated>2020-04-30T16:50:00+00:00</updated><id>https://cthoyt.com/2020/04/30/public-bel-content</id><content type="html" xml:base="https://cthoyt.com/2020/04/30/public-bel-content.html"><![CDATA[<p>While many researchers have a pathway or pathology of interest, their first time
curating content in the Biological Expression Language (BEL) may seem
intimidating. This post lists several disease maps and BEL content sources that
are directly available for re-use.</p>

<h2 id="manually-curated-bel">Manually Curated BEL</h2>

<p>The <a href="https://github.com/pybel/bel-repository">bel-repository</a> Python package has
the job of taking BEL content that lives in a GitHub repository and making it
easy to share with other people. It leverages <code class="language-plaintext highlighter-rouge">pybel</code> and <code class="language-plaintext highlighter-rouge">tox</code> to pre-compile
BEL content and provide summaries that are auto-hosted by GitHub (when
configured properly). The following is a short but growing list of some
resources that are available through GitHub.</p>

<h3 id="selventa-large--small-corpora-">Selventa Large + Small Corpora <a href="https://zenodo.org/badge/latestdoi/199500332"><img src="https://zenodo.org/badge/199500332.svg" alt="DOI" /></a></h3>

<p>Along with their release of the OpenBEL Framework in 2012, Selventa published
two files
<a href="https://github.com/OpenBEL/openbel-framework-resources/tree/latest/knowledge">to GitHub</a>
that showcased the range of biological phenomena that could be expressed in BEL
with a slight focus on atherosclerosis. They were excitingly named the “Selventa
Small Corpus” and “Selventa Large Corpus”.</p>

<p>Because Selventa dissolved in 2016 and the belframework.org domain expired in
mid-2019, the files have been updated and redistributed on GitHub at
<a href="https://github.com/cthoyt/selventa-knowledge">cthoyt/selventa-knowledge</a> under
the Creative Commons Attribution-Non-Commercial-ShareAlike 3.0 Unported License,
same as the original. It can be installed in Python directly with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>git+https://github.com/cthoyt/selventa-knowledge.git
selventa-knowledge summarize
</code></pre></div></div>

<p>It can be used in Python as a PyBEL
<a href="https://pybel.readthedocs.io/en/latest/reference/struct/datamodel.html#pybel.BELGraph">BEL graph</a>
with:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">selventa_knowledge</span>
<span class="n">selventa_graph</span> <span class="o">=</span> <span class="n">selventa_knowledge</span><span class="p">.</span><span class="n">get_graph</span><span class="p">()</span>
<span class="n">selventa_graph</span><span class="p">.</span><span class="n">summarize</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="epicom-">EpiCom <a href="https://zenodo.org/badge/latestdoi/189166127"><img src="https://zenodo.org/badge/189166127.svg" alt="DOI" /></a></h3>

<p>This knowledge graph describes putative mechanisms involved in the pathogenesis
of Epilepsy. It was published by Hoyt and Domingo-Fernández <em>et al.</em> (2018) in
<a href="https://doi.org/10.1093/database/bay050">A systematic approach for identifying shared mechanisms in epilepsy and its comorbidities</a>.</p>

<p>It is available on GitHub under
<a href="https://github.com/neurommsig-epilepsy/neurommsig-epilepsy">neurommsig-epilepsy/neurommsig-epilepsy</a>
or can be installed directly with Python using:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>neurommsig-epilepsy
neurommsig-epilepsy summarize
</code></pre></div></div>

<p>After installation, it can be directly used with PyBEL like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">neurommsig_epilepsy</span>
<span class="n">epilepsy_graph</span> <span class="o">=</span> <span class="n">neurommsig_epilepsy</span><span class="p">.</span><span class="n">get_graph</span><span class="p">()</span>
<span class="n">epilepsy_graph</span><span class="p">.</span><span class="n">summarize</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="conib-">CONIB <a href="https://doi.org/10.5281/zenodo.3385895"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.3385895.svg" alt="DOI" /></a></h3>

<p>The Curation of Neurodegeneration in BEL (CONIB) encodes biological phenomena
related to tauopathies in the context of neurodegeneration.</p>

<p>It is available on GitHub under
<a href="https://github.com/pharmacome/conib">pharmacome/conib</a> or can be installed
directly with Python using:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>git+https://github.com/pharmacome/conib.git
conib summarize
</code></pre></div></div>

<p>After installation, it can be directly used with PyBEL like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">conib</span>
<span class="n">conib_graph</span> <span class="o">=</span> <span class="n">conib</span><span class="p">.</span><span class="n">get_graph</span><span class="p">()</span>
<span class="n">conib_graph</span><span class="p">.</span><span class="n">summarize</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="hemekg-">HemeKG <a href="https://zenodo.org/badge/latestdoi/202482655"><img src="https://zenodo.org/badge/202482655.svg" alt="DOI" /></a></h3>

<p>The Heme Knowledge Graph (HemeKG) encodes heme pathogenicity and pathway
dysregulation in the context of hemolytic disorders. It was published by Humayun
<em>et al.</em> in
<a href="https://doi.org/10.3389/fbioe.2020.00074">A computational approach for mapping heme biology in the context of hemolytic disorders</a>.</p>

<p>It is available on GitHub under
<a href="https://github.com/hemekg/hemekg">hemekg/hemekg</a> or can be installed directly
with Python using:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>hemekg
hemekg summarize
</code></pre></div></div>

<p>After installation, it can be directly used with PyBEL like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">hemekg</span>
<span class="n">heme_graph</span> <span class="o">=</span> <span class="n">hemekg</span><span class="p">.</span><span class="n">get_graph</span><span class="p">()</span>
<span class="n">heme_graph</span><span class="p">.</span><span class="n">summarize</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="covid-19-knowledge-graph-">COVID-19 Knowledge Graph <a href="https://doi.org/10.5281/zenodo.3748950"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.3748950.svg" alt="DOI" /></a></h3>

<p>The COVID-19 Knowledge graph encodes biology surrounding the infection of the
novel coronavirus. It was published by Domingo-Fernández <em>et al.</em> in
<a href="https://doi.org/10.1101/2020.04.14.040667"> COVID-19 Knowledge Graph: a computable, multi-modal, cause-and-effect knowledge model of COVID-19 pathophysiology</a>.</p>

<p>It is available on GitHub under
<a href="https://github.com/covid19kg/covid19kg">covid19kg/covid19kg</a> or can be
installed directly with Python using:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>git+https://github.com/covid19kg/covid19kg.git
covid19kg summarize
</code></pre></div></div>

<p>After installation, it can be directly used with PyBEL like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">covid19kg</span>
<span class="n">covid19_graph</span> <span class="o">=</span> <span class="n">covid19kg</span><span class="p">.</span><span class="n">get_graph</span><span class="p">()</span>
<span class="n">covid19_graph</span><span class="p">.</span><span class="n">summarize</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="bio2bel">Bio2BEL</h2>

<p>The <a href="https://github.com/bio2bel/bio2bel">Bio2BEL</a> ecosystem is built on top of
PyBEL to convert as many reasonable structured data sources as possible into
BEL, automatically. A
<a href="https://bio2bel.readthedocs.io/en/latest/repositories.html">list of most of the available repositories</a>
is available in the documentation. A few notable examples are included here.
Notably, metadatabases like Pathway Commons will not be supported by Bio2BEL,
because it’s almost always better to get the source data which is much higher
granular. A
<a href="https://www.biorxiv.org/content/biorxiv/early/2019/05/08/631812.full.pdf">pre-print</a>
describing the Bio2BEL philosophy and summarizing the content in it is available
on <em>bioRxiv</em>.</p>

<h3 id="causal-biological-networks-database">Causal Biological Networks Database</h3>

<p>The <a href="http://causalbionet.com/">Causal Biological Networks database</a> provides a
swath of ground-truth biology. It has human, rat, and mouse networks that can be
downloaded from the main page in a bespoke (and undocumented) JGF variant.
Luckily, an importer can be found in the
<a href="https://pybel.readthedocs.io/en/latest/reference/io.html?highlight=CBN#pybel.from_cbn_jgif_file">PyBEL documentation</a>
with instructions for unzipping the downloads and loading them into PyBEL with
<code class="language-plaintext highlighter-rouge">pybel.from_cbn_jgif_file()</code>.</p>

<p>The content has also been pre-processed and is available at
<a href="https://github.com/pybel/cbn-bel">https://github.com/pybel/cbn-bel</a></p>

<h3 id="hetionet">Hetionet</h3>

<p>Hetionet is a large biomedical knowledge graphs created for drug repositioning.</p>

<blockquote>
  <p><a href="https://doi.org/10.7554/eLife.26726">Systematic integration of biomedical knowledge prioritizes drugs for repurposing</a>
Daniel S Himmelstein, Antoine Lizee, Christine Hessler, Leo Brueggeman,
Sabrina L Chen, Dexter Hadley, Ari Green, Pouya Khankhanian, Sergio E
Baranzini <em>eLife</em> (2017-09-22) DOI: <code class="language-plaintext highlighter-rouge">10.7554/eLife.26726</code></p>
</blockquote>

<p>An export of Hetionet as BEL can be found at
<a href="https://github.com/pybel/hetionet-bel">https://github.com/pybel/hetionet-bel</a>.</p>

<h3 id="openbiolink-">OpenBioLink <a href="https://doi.org/10.5281/zenodo.3834052"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.3834052.svg" alt="DOI" /></a></h3>

<p>OpenBioLink is a framework that produces biomedical knowledge graphs for link
prediction.</p>

<blockquote>
  <p><a href="https://doi.org/10.1093/bioinformatics/btaa274">OpenBioLink: A benchmarking framework for large-scale biomedical link prediction</a>
Breit, A., Ott, S., Agibetov, A., &amp; Samwald, M. <em>Bioinformatics</em> (2020) DOI:
<code class="language-plaintext highlighter-rouge">10.1093/bioinformatics/btaa274</code></p>
</blockquote>

<h3 id="kegg-wikipathways-and-reactome">KEGG, WikiPathways, and Reactome</h3>

<p>The <a href="https://github.com/PathwayMerger">PathMe</a> package converts KEGG, Reactome,
and WikiPathways to BEL. Results may vary, because the variety of XML, BioPAX,
and GPML/RDF used by the source databases changes all of the time, but this is a
trove of content from other manual curation efforts. It was published by
Domingo-Fernández <em>et al</em> in
<a href="https://bmcbioinformatics.biomedcentral.com/articles/10.1186/s12859-019-2863-9"><em>BMC Bioinformatics</em></a>.</p>

<h2 id="leaving-the-pybel-ecosystem">Leaving the PyBEL Ecosystem</h2>

<p>While each repository distributes the source BEL scripts along with pre-compiled
BEL JSON for easy use with PyBEL, some users may want to convert the content for
other purposes. For example, you may want to upload it to one of many services
that PyBEL interacts with.</p>

<h3 id="upload-to-bel-commons">Upload to BEL Commons</h3>

<p>BEL Commons is a free, open-source platform for hosting BEL content. It includes
the ability to interactively explore nodes, edges, and networks along with their
associated metadata as well as enrich them with content from Bio2BEL.</p>

<blockquote>
  <p><strong>Warning</strong> BEL Commons was originally developed and published in an academic
capacity at Fraunhofer. While they hosted a
<a href="https://bel-commons-dev.scai.fraunhofer.de">public instance</a> to support its
original publication, it was permanently shut down in 2020.</p>
</blockquote>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">covid19kg</span>
<span class="n">graph</span> <span class="o">=</span> <span class="n">covid19kg</span><span class="p">.</span><span class="n">get_graph</span><span class="p">()</span>

<span class="kn">import</span> <span class="nn">pybel</span>
<span class="n">pybel</span><span class="p">.</span><span class="n">to_bel_commons</span><span class="p">(</span><span class="n">graph</span><span class="p">,</span> <span class="n">host</span><span class="o">=</span><span class="s">'...'</span><span class="p">,</span> <span class="n">user</span><span class="o">=</span><span class="s">'...'</span><span class="p">,</span> <span class="n">password</span><span class="o">=</span><span class="s">'...'</span><span class="p">)</span>
</code></pre></div></div>

<p>If you would like to host your own instance of BEL Commons, it can be downloaded
from GitHub
(<a href="https://github.com/bel-commons/bel-commons">bel-commons/bel-commons</a>) and
hosted easily with Docker. If you are hosting your own instance, all you have to
do is use the <code class="language-plaintext highlighter-rouge">host</code> keyword. See the
<a href="https://pybel.readthedocs.io/en/latest/reference/io.html#module-pybel.io.bel_commons_client">pybel.to_bel_commons()</a>
documentation.</p>

<h3 id="upload-to-biodati-studio">Upload to BioDati Studio</h3>

<blockquote>
  <p><strong>Warning</strong> After a relatively short stint, BioDati was shut down some time in
2021, likely coinciding with the departure of William Hayes to go to Sage
Therapeutics. Therefore, this section corresponds to infrastructure that no
longer exists, but it will remain in this post for posterity.</p>
</blockquote>

<p>BioDati is a paid, closed-source platform for hosting BEL content. However, they
do have a demo instance running at https://studio.demo.biodati.com with which
the examples in this module will be described.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">covid19kg</span>
<span class="n">graph</span> <span class="o">=</span> <span class="n">covid19kg</span><span class="p">.</span><span class="n">get_graph</span><span class="p">()</span>

<span class="kn">import</span> <span class="nn">pybel</span>
<span class="n">pybel</span><span class="p">.</span><span class="n">to_biodati</span><span class="p">(</span><span class="n">graph</span><span class="p">,</span> <span class="n">host</span><span class="o">=</span><span class="s">'https://nanopubstore.demo.biodati.com'</span><span class="p">,</span> <span class="n">user</span><span class="o">=</span><span class="s">'...'</span><span class="p">,</span> <span class="n">password</span><span class="o">=</span><span class="s">'...'</span><span class="p">)</span>
</code></pre></div></div>

<p>More information on uploading to BioDati can be found in the documentation of
<a href="https://pybel.readthedocs.io/en/latest/reference/io.html#module-pybel.io.biodati_client">pybel.to_biodati()</a>.</p>

<h3 id="conversion-to-indra">Conversion to INDRA</h3>

<p>INDRA is an excellent suite for automated model assembly developed in the
Laboratory of Systems Pharmacology at Harvard Medical School. PyBEL can convert
content to INDRA with the following code:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">covid19kg</span>
<span class="n">graph</span> <span class="o">=</span> <span class="n">covid19kg</span><span class="p">.</span><span class="n">get_graph</span><span class="p">()</span>

<span class="kn">import</span> <span class="nn">pybel</span>
<span class="n">statements</span> <span class="o">=</span> <span class="n">pybel</span><span class="p">.</span><span class="n">to_indra_statements</span><span class="p">(</span><span class="n">graph</span><span class="p">)</span>
</code></pre></div></div>

<hr />

<p>PyBEL has tons of other I/O formats, especially for analytical tools like
<a href="http://hipathia.babelomics.org">HiPathia</a>,
<a href="https://bioconductor.org/packages/release/bioc/html/SPIA.html">SPIA</a>,
<a href="https://github.com/pykeen/pykeen">PyKEEN</a>, and others that aren’t mentioned
here, but can be found in its documentation
<a href="https://pybel.readthedocs.io/en/latest/reference/io.html">here</a>.</p>

<p>I hope you’re able to find some of this content useful! If you know about any
other publicly available BEL content, please let me know, so I can update this
post. All of my contact info is below. Or make a
<a href="https://github.com/cthoyt/cthoyt.github.io/edit/master/_posts/2020-04-30-public-bel-content.md">pull request against this page</a>
directly.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="BEL" /><category term="PyBEL" /><summary type="html"><![CDATA[While many researchers have a pathway or pathology of interest, their first time curating content in the Biological Expression Language (BEL) may seem intimidating. This post lists several disease maps and BEL content sources that are directly available for re-use.]]></summary></entry><entry><title type="html">An Incomplete History of Selventa and the Biological Expression Language (BEL)</title><link href="https://cthoyt.com/2020/04/28/history-of-bel.html" rel="alternate" type="text/html" title="An Incomplete History of Selventa and the Biological Expression Language (BEL)" /><published>2020-04-28T08:00:00+00:00</published><updated>2020-04-28T08:00:00+00:00</updated><id>https://cthoyt.com/2020/04/28/history-of-bel</id><content type="html" xml:base="https://cthoyt.com/2020/04/28/history-of-bel.html"><![CDATA[<p>The company and community that surround the Biological Expression Language (BEL)
are enigmatic, to say the least. This post represents the best I could do to
tell the history of Selventa and BEL.</p>

<p>If you’ve read my last few posts, you know that I’m making the best of
quarantine time by being quite silly with the way that I’m talking about
science. I have a habit of injecting opinion, but to tell the story of Selventa
and the Biological Expression Language, I will try to refrain whenever possible.
In the following, I chronicle the history of Selventa, the people who worked
there, and the community that emerged from their work. It is not a complete
history.</p>

<p>There are obvious things that I can not know about the inner workings of
Selventa, despite the fact that I used to walk by their building on the way to
my internship at Pfizer on Cambridge Park Drive between 2013-14. There are both
things that I’ve learned through by word of mouth that I think are worth sharing
and that I think are best considered gossip. There are things that I know
because of my time and work at Fraunhofer that I’m not able to share due to
non-disclosure agreements, too, though a secondary purpose of this post is to
show off just how many people have been involved either directly or tangentially
in this community, so I will try my best to share what I can. There were even a
few cases where I found references to things I thought were under NDA on the
internet, so I feel perfectly fine with sharing. I’ve put references to
everything that can be qualified throughout this post.</p>

<p>There are also things that I’m aware of that I’ve chosen to exclude because of
their lack of relevance, quality, impact, or contribution the community. For
example, I have chosen to exclude some papers that have claimed to use BEL for
modeling purposes, but have not shared their underlying knowledge graphs. I have
also chosen to exclude papers that present new algorithms for BEL graphs that do
not share code or examples.</p>

<p>Finally, I am not perfect and do not know everything. I’m certain I’ve missed
something important, because it turns out that a lot of people have been working
on BEL in the last twenty years. I would be happy to accept suggestions to add
things. If a part of this post is about you and you think that I have portrayed
you incorrectly, please get in touch. Finally, I plan on maintaining this post
as time goes on and more cool things are published in this community. And so, we
being on a dark and stormy night in 2001….</p>

<h3 id="2001">2001</h3>

<p><strong>2001</strong> Selventa is
<a href="https://www.flagshippioneering.com/companies/selventa">founded</a> by
<a href="https://www.linkedin.com/in/noubarafeyan/">Noubar Afeyan</a> as Genstruct, Inc.
The concept was to create a computational complement to the company
<a href="https://bg-medicine.com">Beyond Genomics, Inc.</a> - which was a systems biology
company also founded by AGTC (a predecessor fund to Flagship).</p>

<h3 id="2002">2002</h3>

<p><strong>Summer 2002</strong> Keith Elliston is hired by Flagship Ventures (later renamed
Flagship Pioneering) to work with the founding team of Genstruct (Navin Chandra,
<a href="https://www.linkedin.com/in/justinsun/">Justin Sun</a>,
<a href="https://www.linkedin.com/in/tedslater/">Ted Slater</a>,
<a href="https://www.linkedin.com/in/dexter-pratt-80a0901/">Dexter Pratt</a>) to find a
technology and business model for the company. Keith was brought in by
<a href="https://www.linkedin.com/in/james-serum-b10ba/">Jim Serum</a>, who co-founded
<a href="https://www.crunchbase.com/organization/viaken-systems">Viaken Systems</a> with
Elliston, and was on the board of Genstruct.</p>

<p><strong>October 2002</strong> Keith submits a funding proposal and business plan for
Genstruct, based upon a new approach to using artificial intelligence for
biological networks proposed by Navin Chandra and developed by the team. The
board approves the plan, approves investment in Genstruct, and hires Keith as
the CEO.</p>

<h3 id="2003">2003</h3>

<p><strong>January 2003</strong> Genstruct does its first commercial project, a pilot project
with Pfizer, where they successfully identified the mechanism of action of an
unknown cancer drug. This leads to a more than 10 year collaboration between the
companies.</p>

<p><strong>Spring 2003</strong>. Biological Expression Language is created by Dexter Pratt,
Navin Chandra, Keith Elliston, and Ted Slater. This development is based on work
done by Navin at
<a href="https://en.wikipedia.org/wiki/Perot_Systems">Perot Systems and Nets</a>, work done
by Dexter on <a href="https://www.cyc.com/archives/glossary/cycl-sentence">CycL</a> with
<a href="https://www.cyc.com">Cycorp</a>, and on biological ontologies by Keith and Ted.</p>

<p><strong>August 20th, 2003</strong> Genstruct files a
<a href="https://patents.google.com/patent/US20050038608A1/en">patent</a> that outlines
many of the core ideas of modern systems biology years before they became
mainstream. While it doesn’t mention BEL explicitly, it’s obvious that many of
the ideas in this application became part of the BEL standard when it was later
released to the public. The patent was accepted 8 years later in 2011. Thanks to
Ted Slater for bringing attention to this!</p>

<p><strong>September 8th, 2003</strong> Genstruct
<a href="https://www.crunchbase.com/funding_round/selventa-series-a--1f3d3583">raises $6.5M</a>
in a Series A with lead investors
<a href="https://www.flagshippioneering.com/companies/selventa">Flagship Pioneering</a> and
<a href="https://www.pappas-capital.com/portfolio/selventa/">A.M. Pappas &amp; Associates</a>,
during which they acquire the Pappas portfolio company
<a href="https://www.crunchbase.com/organization/incellico-inc">Incellico, Inc.</a> (led by
<a href="https://www.linkedin.com/in/johnwilbanks/">John Wilbanks</a> and
<a href="https://www.linkedin.com/in/toby-segaran-80bbb9/">Toby Segaran</a>).</p>

<h3 id="2004">2004</h3>

<p><strong>2004</strong> Toby Segaren, working with Justin Sun and other developers at the
company, develop the first practical version of the Genstruct Inference Engine.
The inference engine implements a reverse causal reasoning using a graph-based
approach, and identifies upstream causes (mechanisms) for downstream
observations (state changes).
<a href="https://www.linkedin.com/in/bill-ladd-a8b258/">Bill Ladd</a> develops the
statistical methods that power the inference engine and that are used to
evaluate the results of simulations.</p>

<p><strong>February 24, 2004</strong> Genstruct appoints
<a href="https://www.linkedin.com/in/douglas-lauffenburger-34a0282">Doug Lauffenberg</a>,
Director of the Biological Engineering Division and Uncas &amp; Helen Whitaker
Professor of Biological Engineering, Chemical Engineering and Biology at the
Massachusetts Institute of Technology (MIT), to the board of directors.</p>

<p><strong>November 2004</strong> Genstruct and Pfizer extend and expand their partnership,
applying the Genstruct platform to various R&amp;D and Toxicology programs
throughout Pfizer.
(https://www.genomeweb.com/archive/genstruct-pfizer-expand-research-partnership#.X3jczJNKh24)</p>

<h3 id="2006">2006</h3>

<p><strong>August 2006</strong> Genstruct and GSK extend their partnership to apply the
Genstruct platform to define compound mechanisms of action in Oncology
(https://www.fdanews.com/articles/61568-genstruct-extends-collaboration-in-cancer-with-glaxosmithkline)</p>

<p><strong>Sometime in 2006</strong> Gensruct reaches cashflow break even
(<a href="https://www.linkedin.com/in/keithelliston/">ref</a>). Keith Elliston’s LinkedIn
profile claims that it was cash flow positive in 2008 and 2009.</p>

<h3 id="2008">2008</h3>

<p><strong>April 30, 2008</strong> Genstruct and Sirtris win the BioIT World Best Practices
Award for their work using the Genstruct Platform to identify the mechanisms of
action for the Sirtris Sirt1 activators. Sirtris was acquired by GSK to further
develop its Sirt1 activators.</p>

<p><strong>July 2008</strong> Genstruct begins a collaboration with Manuel Pietsch and his group
on the use of the Genstruct technology to assess the stength and extent of
toxicity using network analysis</p>

<h3 id="2009">2009</h3>

<p><strong>2009</strong> Genstruct develops the “Network Perturbation Amplitude” algorithm,
based on early work done by Jim Watters on pathway expression activation.</p>

<p><strong>September 2009</strong> Board appoints Chris Varma from Flagship Ventures as
Executive Chairman.</p>

<h3 id="2010">2010</h3>

<p><strong>Jan 2010</strong> Keith Elliston resigns from the company,
<a href="https://www.linkedin.com/in/chris-varma-59828a4/">Chris Varma</a> named CEO
(though this is missing from his LinkedIn profile).</p>

<p><strong>May 17th, 2010</strong> Genstruct
<a href="https://www.crunchbase.com/funding_round/selventa-series-unknown--a7424e79#section-overview">raises $500K</a>
in a Venture Round. This is the second of three rounds of funding, the last of
which will occur in late 2011.</p>

<p><strong>May 2010</strong> Genstruct hires
<a href="https://www.linkedin.com/in/davidd12/">David de Graaf</a> as CSO.</p>

<p><strong>July 2010</strong> Chris Varma leaves Genstruct to join Third Rock Ventures.</p>

<p><strong>November 10th, 2010</strong> Genstruct is rebranded as Selventa
(<a href="https://www.businesswire.com/news/home/20101130005216/en/Genstruct-Selventa-Company-Launches-Re-branding-Effort">ref</a>)</p>

<p><strong>December 2010</strong> David de Graaf named CEO.</p>

<h3 id="2011">2011</h3>

<p><strong>June 23rd, 2011</strong> Selventa makes its
<a href="https://twitter.com/Selventa/status/84290855413891072?s=20">first tweet</a> from
<a href="https://twitter.com/selventa">@selventa</a>.</p>

<p><strong>November 29th, 2011</strong> Selventa
<a href="https://www.crunchbase.com/funding_round/selventa-series-unknown--7be2c689#section-overview">raises $5M</a>
in its final Venture Round.</p>

<h3 id="2012">2012</h3>

<p><strong>April 27th, 2012</strong> The OpenBEL Consortium begins and establishes
<a href="http://openbel.org">http://openbel.org</a> as a community resource
(<a href="https://wiki.openbel.org/display/BLDV10/BEL+History">ref</a>, written by Jordan
Hourani on July 9th, 2012). There has been (and remains) great conflation
between the name of the Biological Expression Language, the OpenBEL Framework
(see next line), and the OpenBEL Consortium. Skipping ahead a few years: with
the later deprecation and abandonment of the OpenBEL Framework, whose
organization on GitHub also hosted the OpenBEL Consortium’s website, it became
unclear how maintenance should proceed.</p>

<p><strong>May 18th, 2012</strong> Selventa makes their
<a href="https://github.com/OpenBEL/openbel-framework/commit/09dee5283f2de5a5302d8bece14321b8fd268ef6">first public commit</a>
to their open source Java ecosystem, the
<a href="https://github.com/openbel/openbel-framework">OpenBEL Framework</a>.</p>

<p><strong>May 23rd, 2012</strong> Kevin Davies writes
<a href="http://www.bio-itworld.com/2012/05/23/ring-my-bel-selventa-releases-biological-expression-language.html">Ring My BEL: Selventa Releases Biological Expression Language</a>
to be published on the Bio-IT World Website</p>

<p><strong>May 23rd, 2012</strong> OpenBEL joins Twitter <a href="https://twitter.com/openbel">@openbel</a>
and posts
<a href="https://twitter.com/Selventa/status/205395108001091585?s=20">its first tweet</a>,
a re-tweet of a Selventa link to the previously mentioned Kevin Davies article.
It’s not clear who was the author or who currently holds the credentials. It’s
also not clear at this time whether the Twitter account was for the OpenBEL
framework, or the OpenBEL Consortium, which Selventa would organize later that
year.</p>

<p><strong>May 31st, 2012</strong> The
<a href="https://bmcsystbiol.biomedcentral.com/articles/10.1186/1752-0509-6-54">first</a>
and
<a href="https://www.sciencedirect.com/science/article/pii/S1359644611004259?via%3Dihub">second</a>
papers on the Network Perturbation Amplitude were published simultaneously in
different journals, respectively authored by Florian Martin (Philip Morris
International) and Julia Hoeng (Philip Morris international). Each paper
contained several authors from both Philip Morris International (PMI) and
Selventa.</p>

<p><strong>June 18th, 2012</strong> Selventa discloses that there is interest/collaboration with
several pharmaceutical and software companies as well as academic, governmental
and non-profit groups interested and plans to organize an external non-profit
organization (The OpenBEL Consortium) to facilitate the community around the
Biological Expression Language
(<a href="https://www.biospace.com/article/releases/selventa-releases-openbel-as-open-source-and-encourages-the-community-to-participate-in-an-openbel-consortium-/">ref 1</a>,
<a href="https://www.businesswire.com/news/home/20120618005647/en/Selventa-Releases-OpenBEL-Open-Source-Encourages-Community">ref 2</a>).
This list includes <a href="https://www.pfizer.com/">Pfizer</a>,
<a href="https://www.pfizer.com/">Merck</a>,
<a href="https://www.thomsonreuters.com/en.html">Thomson Reuters</a> (the department that
was involved was later spun off into
<a href="https://clarivate.com/">Clarivate Analytics</a> to support the
Metabase/<a href="https://clarivate.com/cortellis/solutions/early-research-intelligence-solutions/">Metacore</a>),
<a href="https://www.scai.fraunhofer.de/en.html">Fraunhofer</a>,
<a href="https://hms.harvard.edu/">Harvard Medical School</a>, IDBS (listed, but I wasn’t
able to figure out who they were),
<a href="https://www.linguamatics.com/">Linguamatics</a>,
<a href="https://www.crunchbase.com/organization/entagen">Entagen (since dissolved)</a>.</p>

<p><strong>July 25th, 2012</strong> The
<a href="https://groups.google.com/forum/#!forum/openbel-discuss">OpenBEL Google group</a>
is created. Through 2020, it remains a semi-active place for discussion in the
BEL Community.</p>

<p><strong>August 9th, 2012</strong>
<a href="https://www.linkedin.com/in/david-fryburg-m-d-ba67995/">David Fryburg</a>,
Selventa’s Chief Medical Officer from 2011-2015, authored a
<a href="https://pubmed.ncbi.nlm.nih.gov/29768793/">company profile</a> in <em>Future
Medicine</em> (paywall).</p>

<p><strong>Fall 2012</strong> <a href="https://www.linkedin.com/in/tedslater/">Ted Slater</a>, Selventa
V.P. of Knowledge Engineering from 2002-2004 and later returning as CTO between
2012-2013, along with
<a href="https://www.linkedin.com/in/diane-song-0718b46/">Dr. Diane H. Song</a>, marketing,
publish
<a href="https://www.ddw-online.com/informatics/p191019-biological-expression-language-(bel):ringing-in-a-common-language-for-the-life-sciences.html">
Biological Expression Language (BEL): Ringing In A Common Language For The Life
Sciences</a> in the Fall 2012 issue of <em>Drug Discovery World</em> as well as a
companion piece
<a href="https://www.ddw-online.com/media/32/6950/saved-by-the-bel---ringing-in-a-common-language-for-the-life-sciences.pdf">Saved by the BEL - ringing in a common language for the life sciences</a></p>

<h3 id="2013">2013</h3>

<p><strong>August 26th, 2013</strong> OpenBEL becomes a Linux Foundation Collaborative Project
(<a href="https://www.linuxfoundation.org/press-release/2013/08/life-sciences-information-framework-openbel-to-become-a-linux-foundation-collaborative-project/">ref 1</a>,
<a href="https://opensource.com/health/13/8/openbel-joins-linux-foundation">ref 2</a>) as a
first attempt at identifying external funding. I was unable to find evidence of
when this ended, but I have heard from members at the time that it was
ultimately unsuccessful and dissolved.</p>

<p><strong>October 13th, 2013</strong> Following the establishment of the
<a href="http://www.sbvimprover.com/">sbv IMPROVER</a> initiative by Philip Morris
International and subsequent publications in
<a href="http://www.nature.com/nbt/journal/v29/n9/full/nbt.1968.html">2011</a>,
<a href="http://bioinformatics.oxfordjournals.org/content/28/9/1193">2012</a>, and
<a href="https://www.tandfonline.com/doi/full/10.4161/sysb.26324">2013</a>, their first
Network Verification Challenge was held between October 2013 and March 2014. It
was published by <a href="https://www.linkedin.com/in/ansarisam/">Sam Ansari</a> (PMI; with
shared first authorship for all members sbv IMPROVER team) and marked the first
use of BEL in the sbv IMPROVER
(<a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3798292/">ref</a>).</p>

<p><strong>November 2nd, 2013</strong>
<a href="https://www.linkedin.com/in/williamshayes/">William Hayes</a>, CTO of Selventa
from 2012-2016, released the
<a href="https://rubygems.org/gems/bel/versions/0.1.0">first version</a> of the <code class="language-plaintext highlighter-rouge">bel.rb</code>
Ruby package on <a href="https://rubygems.org/gems/bel">rubygems</a>. This likely marked
the end of Selventa’s support for the OpenBEL Framework, as both Java was going
out of style and their codebase had not aged well. However, it’s generally hard
to tell when software projects are dead. The maintainers,
<a href="https://www.linkedin.com/in/anthonybargnesi/">Anthony Bargnesi</a> and
<a href="https://www.linkedin.com/in/nick-bargnesi-bbb85549/">Nick Bargnesi</a>, continued
to make intermittent maintenance commits to the OpenBEL Framework’s codebase
through
<a href="https://github.com/OpenBEL/openbel-framework/commit/b1ce7496b1e6b22798cc7a53c20c6414cea5acc1">June 24th, 2015</a>.</p>

<p><strong>November 23rd, 2013</strong>
<a href="https://www.linkedin.com/in/natalie-leech-catlett/">Natalie Catlett</a> (Selventa)
and colleagues publish the
<a href="https://bmcbioinformatics.biomedcentral.com/articles/10.1186/1471-2105-14-340">Reverse Causal Reasoning algorithm</a>
in <em>BMC Bioinformatics</em>, this time with no co-authors from PMI.</p>

<h3 id="2014">2014</h3>

<p><strong>January 23rd, 2014</strong> With the death of the OpenBEL Framework in sight, the
<a href="https://twitter.com/openbel">@openbel</a> Twitter account
<a href="https://twitter.com/OpenBEL/status/423083988291313664?s=20">began to publicize</a>
the <code class="language-plaintext highlighter-rouge">bel.rb</code> Ruby package.</p>

<p><strong>February 2014</strong> Ted Slater publishes a review of BEL,
<a href="https://doi.org/10.1016/j.drudis.2013.12.011">Recent advances in modeling languages for pathway maps and computable biological networks</a>
which continues to serve as the appropriate paper to reference for the
Biological Expression Language itself. When you skip ahead it might seem obvious
that I’m collating information to put together a new reference paper describing
the updates from the following six years.</p>

<p><strong>July 11, 2014</strong> Florian Martin (Philip Morris International) and colleagues
published their
<a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4227138/">third paper</a> (I think;
they have been quite prolific in the 2010’s) paper describing the Network
Perturbation Amplitude analysis, this time with no co-authors from Selventa.</p>

<h3 id="2015">2015</h3>

<p><strong>Sometime between 2015-2017</strong> With the withdrawn of support from Christoph
Brockel (sometime between 2015 and 2017, when he left Pfizer), Pfizer divests
from BEL. Its internal BEL-based analytical platform, the
<a href="https://academic.oup.com/bioinformatics/article/28/8/1114/195407">Causal Reasoning Engine</a>
and its underlying knowledgebase are publicized, but never released.</p>

<p><strong>January 23rd, 2015</strong> In concert with the sbv IMPROVER’s adoption of BEL from
PMI,
the<a href="https://biocreative.bioinformatics.udel.edu/tasks/biocreative-v/">fifth iteration of the BioCreative Challenge</a>
hosts its first
<a href="https://biocreative.bioinformatics.udel.edu/tasks/biocreative-v/track-4-bel-task/">BEL-specific text mining challenge</a>.
It was organized by OntoGene (Fabio Rinaldi), the sbv IMPROVER/PMI (Sam Ansari,
Julia Hoeng), and Fraunhofer (Juliane Fluck, Martin Hofmann Apitius) following
the footsteps of the sbvIMPROVER network verification challenge.</p>

<p><strong>March 2015</strong> David de Graaf steps out of CEO role at Genstruct, joins Flagship
Venturelabs.</p>

<p><strong>Sometime before April 2017</strong> The second iteration of the sbv IMPROVER’s
Network Verification Challenge was hosted with a focus on COPD. It’s not clear
when this happened, so I’ll say before April 17th, 2015 because the CausalBioNet
paper (see next bullet point) used the results. On May 15, 2015,
<a href="https://www.linkedin.com/in/st%C3%A9phanie-bou%C3%A9-716a8710/">Stéphanie Boué</a>
(PMI, with shared first authorship with the sbv IMPROVER team) published a
summary of the challenge in
<a href="https://dx.doi.org/10.12688%2Ff1000research.5984.2"><em>F1000 Research</em></a>.</p>

<p><strong>April 17th, 2015</strong>
<a href="https://www.linkedin.com/in/st%C3%A9phanie-bou%C3%A9-716a8710/">Stéphanie Boué</a>
(PMI) publishes the
<a href="http://causalbionet.com/">Causal Biological Networks Database (CausalBioNet)</a>
in
<a href="https://academic.oup.com/database/article/doi/10.1093/database/bav030/2433162"><em>Oxford Database</em></a>
as a summary of the results of the curation done in the second iteration of the
sbv IMPROVER’s Network Verification Challenge. This is the first evidence I
found of the participation of
<a href="https://www.linkedin.com/in/anselmodifabio/">Anselmo Di Fabio</a>’s company,
<a href="http://www.adsworks.com/">Applied Dynamic Solutions (ADS), LLC</a>, in the BEL
Community, though the metadata listed on the paper’s page is wrong so it’s not
obvious which co-authors had affiliations to that organization at the time,
besides Anselmo. Later, William Hayes will join ADS after the dissolution of
Selventa.</p>

<p><strong>June 16th, 2015</strong>
<a href="https://www.pmiscience.com/science/scientists/scientist/justyna-szostak">Justyna Szostak</a>
(PMI) and <a href="https://www.linkedin.com/in/smadan-1/">Sumit Madan</a> (Fraunhofer)
publish the
<a href="http://belief.scai.fraunhofer.de/BeliefDashboard/">BELIEF text mining workflow</a>
following the fifth BioCreative challenge in
<a href="https://academic.oup.com/database/article/doi/10.1093/database/bav057/2433190"><em>Oxford Database</em></a>.
Here is another case where I omitted several other papers following the
BioCreative challenge, as none of the other solutions were accessible. This is
very, very sad in my opinon.</p>

<p><strong>November 9th, 2015</strong> <a href="">Afroza Khanam Irin</a> (Fraunhofer) publishes
<a href="https://www.hindawi.com/journals/jir/2015/737168/">Computational Modelling Approaches on Epigenetic Factors in Neurodegenerative and Autoimmune Diseases and Their Mechanistic Analysis</a>,
which outlined a possible addition to the BEL specification to allow the
codification of epigenetic modifications in BEL. Unfortunately, this proposal
was not considered until the 2018 OpenBEL Consortium meeting, and it is still
under a <em>very slow</em> debate.</p>

<p><strong>Some time between 2015 and 2018</strong> Luc Canard (Sanofi) became involved in
Fraunhofer’s BEL activities throughout the AETIONOMY which cumulated in
<a href="https://www.mdpi.com/1422-0067/16/12/26148/pdf">this publication</a>.</p>

<h3 id="2016">2016</h3>

<p><strong>Sometime in 2016</strong> Selventa dissolves
(<a href="https://www.flagshippioneering.com/companies/selventa">ref</a>). I think this
where this story gets interesting - because it’s also the part that we will be
able to understand the least from an outside perspective. If you serch the
internet for Selventa, you will indeed find lots of well-written press releases
describing the contracts they had made over the years with several notable
biotech and pharmaceutical industries. I’ve heard gossip that the reason it fell
apart was because of mismanagement, but I can’t weigh in on that.</p>

<p><strong>Later than <em>sometime in 2016</em></strong> The Selventa team disperses. Part of the
technology team that supported the OpenBEL Framework moved to
<a href="http://www.adsworks.com/">Applied Dynamic Solutions (ADS), LLC</a>. Some of the
computational biologists moved to
<a href="https://www.patientslikeme.com/">PatientsLikeMe (PLM)</a> (in waves), and I
believe some of the computational team moved directly to Philip Morris
International. Many continued working together, and with the industrial support
for the BEL infrastructure in which they had invested, PMI patronized ADS to
fill the void. Before its dissolution, Dexter Pratt had already moved to UCSD
and begun work on the <a href="http://www.ndexbio.org/">NDEx</a> project. Ex-CEO Keith
Ellison and current CEO David de Graaf continued their careers in VC and
entrepreneurship. Luckily, we have LinkedIn to figure this kind of stuff out.</p>

<p>During its 15 years of operation, Selventa performed an enormous amount of
curation to generate BEL content. I’m not sure what the actual number but I’ve
heard that it has millions of edges in it. After the closure of Selventa, the
intellectual property of the company was sold to
<a href="https://alexion.com/">Alexion</a>. Dexter Pratt asked through the OpenBEL Google
Group where it was, and
<a href="https://www.linkedin.com/in/nimisha-schneider-9a1bb3123/">Nimisha Schneider</a>
claimed that <a href="https://alexion.com/">Alexion</a> might own it now. I’m quite
interested to know about the fate of this trove of curated content, as many of
the Selventa papers alluded to its existence (but the reviewers didn’t seem to
mind that they were publishing academic material while claiming industrial
secrecy. I’m sorry to inject opinion here but I would rather we not have
industrial publications than ones that can’t be reproduced.)</p>

<p><strong>October 1st, 2016</strong> <a href="https://www.linkedin.com/in/smadan-1/">Sumit Madan</a>
publishes the second and final
<a href="https://academic.oup.com/database/article/doi/10.1093/database/baw136/2630528">publication on the BELIEF text mining pipeline</a>.
The lack of updates to this service and lack of further publications might lead
the reader to believe the project is abandoned.</p>

<p><strong>October 9th, 2016</strong> Following a long hiatus in the development of open source
software to support BEL, Charles Tapley Hoyt (Fraunhofer; that’s me!), Andrej
Konotopez (Fraunhofer), and Christian Ebeling (Fraunhofer) release the
<a href="https://github.com/pybel/pybel/releases/tag/v0.1.2">first version</a> of the PyBEL
python package. It was later published in
<a href="https://academic.oup.com/bioinformatics/article/34/4/703/4557184"><em>Oxford Bioinformatics</em></a>.
I may be biased, but I think this marked the beginning of the rejuvination of
the BEL community. Many more developments from me and colleagues at Fraunhofer
follow for the next 3 years through my master’s and doctoral work.</p>

<p><strong>October 25, 2016</strong> Christian Ebeling (Fraunhofer)
<a href="https://www.youtube.com/watch?v=JOCTVQx2y34&amp;list=PLv0yMFogDiDjiizm6acS49ngKyOuc4rdx&amp;index=9&amp;t=0s">presents</a>
PyBEL at the
<a href="https://transmartfoundation.org/2016-annual-meeting/">tranSMART Foundation 2016 Annual Meeting</a>.</p>

<p><strong>November 1st, 2016</strong>
<a href="https://www.linkedin.com/in/danieldomingofdez/">Daniel Domingo Fernández</a>
(Fraunhofer) publishes the
<a href="https://neurommsig.scai.fraunhofer.de/">NeuroMMSig Web server</a> in
<a href="https://academic.oup.com/bioinformatics/article/33/22/3679/3884654"><em>Oxford Bioinformatics</em></a>,
containing one of the first publicly available BEL knowledge graphs as well as
one of the first publicly usable algorithms for BEL graphs.</p>

<p><strong>Sometime in 2016</strong> Cohen Veteran’s BioSciences contracts Fraunhofer to curate
a knowledge graph for PTSD and TBI, supported by PyBEL and BEL Commons
(<a href="https://www.cohenveteransbioscience.org/wp-content/uploads/2016/10/CohenVeteransBioscience-FactSheet.pdf">ref</a>).
In addition, <a href="https://www.exaptive.com/blog/topic/data-visualization">Exaptive</a>
developed additional software for visualization.</p>

<p><strong>Sometime in 2016-2017</strong> Boehringer Ingelheim contracts Fraunhofer to curate a
knowledge graph for psychiatric conditions in the BEL4IMOCEDE project
(<a href="https://www.scai.fraunhofer.de/en/about-us/staff/domingo-fernandez.html">ref 1</a>,
<a href="https://www.linkedin.com/in/asifemon/?originalSubdomain=de">ref 2</a>), supported
by PyBEL and BEL Commons.</p>

<h3 id="2017">2017</h3>

<p><strong>January 24th, 2017</strong> <a href="https://www.linkedin.com/in/asifemon/">Asif Emon</a>
(Fraunhofer) publishes
<a href="https://content.iospress.com/articles/journal-of-alzheimers-disease/jad160222">Using Drugs as Molecular Probes: A Computational Chemical Biology Approach in Neurodegenerative Diseases</a>
which jump started both the cheminformatics side of BEL and inspired the later
Bio2BEL project.</p>

<p><strong>February 22th, 2017</strong>
<a href="https://www.linkedin.com/in/john-bachman-0563725/">John Bachman</a> (Harvard
Medical School; HMS) and
<a href="https://www.linkedin.com/in/benjamin-m-gyori-0519b630/">Ben Gyori</a> (HMS) begin
to integrate PyBEL into the
<a href="https://github.com/sorgerlab/indra/commit/4cb6fb8a85bd7689900be15bad5614fe6951d8c5">INDRA project</a>,
divesting from a previous RDF dump of the Selventa Large Corpus whose provenance
was untraceable (not sure about if this is true or not). INDRA was published in
<a href="https://www.embopress.org/doi/10.15252/msb.20177651"><em>Molecular Systems Biology</em></a>
later that year.</p>

<p><strong>February 26th, 2017</strong> The
<a href="https://biocreative.bioinformatics.udel.edu/tasks/biocreative-vi">sixth BioCreative Challenge</a>
hosts a
<a href="https://biocreative.bioinformatics.udel.edu/tasks/biocreative-vi/track-3/">text mining challenge for BEL</a>.
It was lead by Fraunhofer (Juliane Fluck, Sumit Madan, Martin Hofmann-Apitius)
and Philip Morris International (Justyna Szostak). Again, almost all of the
softwares published for this track did not include a demo.</p>

<p><strong>May 22nd, 2017</strong> The final version of <code class="language-plaintext highlighter-rouge">bel.rb</code>
(<a href="https://rubygems.org/gems/bel/versions/1.1.2">v1.1.2</a>) is released. The code
remains unfunctional, putting an unofficial end to the <code class="language-plaintext highlighter-rouge">bel.rb</code> project. With
the abandonment of the OpenBEL Framework and <code class="language-plaintext highlighter-rouge">bel.rb</code>, PyBEL remains the only
open-source/user-facing BEL software (for a short time, see 2018).</p>

<p><strong>June 11th, 2017</strong> Charles Tapley Hoyt (Fraunhofer) deploys
<a href="https://bel-commons.scai.fraunhofer.de/">BEL Commons</a> as the first interactive
exploration tool for BEL following the abandonment of the OpenBEL Framework and
Cytoscape tool. It is later published in
<a href="https://doi.org/10.1093/database/bay126"><em>Oxford Database</em></a> and
<a href="https://github.com/bel-commons/bel-commons">open source’d</a>.</p>

<p><strong>August 9th, 2017</strong> Fraunhofer
<a href="https://github.com/bio2bel/bio2bel/commit/1dc99d6a807faefde6159773d9f4fce8da3f07f4">starts</a>
the Bio2BEL project open source on <a href="https://github.com/bio2bel/bio2bel">GitHub</a>.
This is a data and knowledge integration effort similar to Pathway Commons for
BioPAX and Bio2RDF for RDF, but with a wider range of knowledge included and
much greater focus on reproducibility and automation. It was later
<a href="https://www.biorxiv.org/content/biorxiv/early/2019/05/08/631812.full.pdf">pre-printed</a>
but in late April 2020, has not yet been accepted for publication.</p>

<h3 id="2018">2018</h3>

<p><strong>January 30, 2018</strong> <a href="https://biodati.com/">BioDati, Inc.</a> officially forms as a
spin-off of ADS and
<a href="https://medium.com/biodati/biodati-inc-announcement-af205a57d0cb">announces</a>
that it is developing a product called BioDati Studio for BEL curation and
visualization.</p>

<p><strong>January 31, 2018</strong> William Hayes (now of ADS/BioDati, Inc.)
<a href="https://medium.com/biodati/bel-bio-announcement-c0a911373757">announces</a> the
launch of the <a href="https://bel.bio/">BEL.bio</a> website as a replacement for the
OpenBEL website. It also announced the
<a href="https://pypi.org/project/bel/0.5.3.dev0/">first release</a> of their <code class="language-plaintext highlighter-rouge">bel</code> python
package that would serve as a backend for their upcoming product.</p>

<p>While the announcement caused some confusion throughout the community as to
whether OpenBEL was a site for the BEL community and whether it should be
deprecated in favor of a new website advertizing another company’s product, some
were happy to see leadership coming from an organization (be it academic or
industrial) that would be able to commit to long term maintenance. Author note:
as a recently started PhD student, I wasn’t in a position to support industrial
usage of PyBEL. If you’ve ever worked with the industry, especially as a
software developer, you know how <em>needy</em> they are. There were users from a
certain company asking for help on a weekly basis until I offered the ultimatum
that they should pay for this kind of consultancy. Ultimately, I appreciated
William and Anselmo’s leadership from BioDati and saw the advantage in having
several complementary software ecosystems. 2018 and 2019 years would be big ones
for me and PyBEL, and our focus would diverge from the nominal curation
interface and network visualization in BioDati.</p>

<p><strong>February 23rd, 2018</strong> Michaela Gündel (Fraunhofer) publishes the
<a href="https://github.com/bel2abm/bel2abm">BEL2ABM</a> workflow in
<a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6022644/"><em>Oxford Database</em></a>,
demonstrating that the use cases of BEL were evolving much further than Selventa
and PMI’s published use cases.</p>

<p><strong>May 14th, 2018</strong> The 2018 OpenBEL Community Meeting occurs coincident to
Bio-IT world in Boston, MA with stakeholders from PMI, Fraunhofer, BioDati,
ADS/BioDati, Harvard Medical School, and several others. Together we nominated
William Hayes (BioDati), Natalie Catlett (now at PatientsLikeMe), John Bachmann
(Harvard Medical School), and Charles Tapley Hoyt (still me, at the time
Fraunhofer) due to our mixed statuses in the industry and academy as well as our
mixed roles as tool developers and tool users to serve as the BEL Language
Committee going forwards. We agreed on guidelines for BEL Enhancement Proposals
and published them at <a href="http://bep.bel.bio/">http://bep.bel.bio</a>. Videos from
this event are available at
<a href="https://www.youtube.com/playlist?list=PLwXD2R4UjER0IfAQpqxOBkSe08gTPws41">https://www.youtube.com/playlist?list=PLwXD2R4UjER0IfAQpqxOBkSe08gTPws41</a>.</p>

<p><strong>Also May 14th, 2018</strong> Christian Ebeling (Fraunhofer) publishes an
<a href="https://atom.io/">Atom</a>) plugin for BEL syntax highlighting at
<a href="https://atom.io/packages/language-bel">https://atom.io/packages/language-bel</a>.</p>

<p><strong>June 4th, 2018</strong> Dexter Pratt (on behalf of the Cytoscape Consortium)
contracts Fraunhofer to improve interoperability between BEL and NDEx through
the PyBEL framework. The results were posted to GitHub in their own
<a href="https://github.com/pybel/pybel-cx">repository</a> but the utility of the CX format
and NDEx interchange were eventually
<a href="https://github.com/pybel/pybel-cx/commit/05463fcdeca612f1e6ab50fafdd117406bbce096">incorporated</a>
into the core of PyBEL.</p>

<p><strong>June 27th, 2018</strong> The
<a href="https://www.sbvimprover.com/challenge-3/sbv-improver-symposium-2018">third</a> sbv
IMPROVER Network Verification Challenge was held at PMI in Neuchatel,
Switzerland. Further improvements were made to the CausalBioNet to investigate
Xenobiotic metabolism and causal biological networks. The three winners were
from Charité, University of Bonn, and the Swiss Institute of Bioinformatics -
demonstrating further the reach of the BEL community and PMI’s excellent
stewardship and engagement.</p>

<p><strong>November 19th, 2018</strong> Charles Tapley Hoyt (Fraunhofer) publishes the
<a href="https://pypi.org/project/pybel-git/0.0.1/">first version</a> of a git-based
workflow that uses Continuous Integration for writing BEL code in a team
environment on <a href="https://github.com/pybel/pybel-git">GitHub</a>. It is later used in
<a href="https://doi.org/10.1093/database/baz068"><em>Oxford Database</em></a> with the rational
enrichment workflow (see below).</p>

<p><strong>December 10th, 2018</strong> Following proposals and reviews submitted after the 2018
OpenBEL Community Meeting, William Hayes
<a href="https://medium.com/bel-news/bel-2-1-enhancements-ac79b078ad5a">publishes</a> the
BEL v2.1 standard on behalf of the BEL Language Committee.</p>

<p><strong>December 11th, 2018</strong> Fraunhofer and Harvard Medical School jointly publish a
workflow using INDRA and PyBEL for rationally enriching BEL graphs in
<a href="https://doi.org/10.1093/database/baz068"><em>Oxford Database</em></a> available on
<a href="https://github.com/bel-enrichment/bel-enrichment">GitHub</a>
(<a href="https://github.com/bel-enrichment/bel-enrichment/commit/853e642fc759d8a98c56e844a8c4d7f66780601b">ref</a>).</p>

<p><strong>December 13th, 2018</strong> Daniel Domingo-Fernández (Fraunhofer) publishes the
ComPath pathway equivalence database and the
<a href="https://compath.scai.fraunhofer.de">ComPath web application</a> in
<a href="https://www.nature.com/articles/s41540-018-0078-8"><em>Nature</em></a> as a first step
towards unifying major public pathway databases in BEL. The source code and
underlying data were published on <a href="https://github.com/ComPath">GitHub</a>.</p>

<h3 id="2019">2019</h3>

<p><strong>February 15th, 2019</strong> Mehdi Ali (University of Bonn) publishes the
<a href="https://github.com/smartdataanalytics/biokeen/">BioKEEN machine learning package</a>
in
<a href="https://academic.oup.com/bioinformatics/article-abstract/35/18/3538/5320556?redirectedFrom=fulltext"><em>Oxford Bioinformatics</em></a>,
introducing the BEL community to an entirely new type of qualitative analysis
and hypothesis generation using BEL.</p>

<p><strong>February 27th, 2019</strong> Dénes Türei (EMBL, University of Heidelberg) makes
<a href="https://github.com/saezlab/pypath/commit/8fd70d235db07884681a0d61607ecb18a3fcbf20#diff-efba33e06ac510f6b81e3e781511f343">the first commit</a>
towards integrating BEL with <a href="http://www.omnipathdb.org/">OmniPath</a>.</p>

<p><strong>September 3rd, 2019</strong> After nearly a decade of non-reproducible papers, PMI
publishes an <a href="https://github.com/philipmorrisintl/NPA">open-source</a> (albeit, R)
implementation of their Network Perturbation Amplitude analysis in
<a href="https://bmcbioinformatics.biomedcentral.com/articles/10.1186/s12859-019-3016-x"><em>BMC Bioinformatics</em></a>.</p>

<p><strong>May 15th, 2019</strong> Daniel Domingo-Fernández (Fraunhofer) publishes
<a href="https://github.com/PathwayMerger">PathMe</a>, the first software integrating KEGG,
Reactome, and WikiPathways (in their variety of formats including XML, BioPAX,
and GPML/RDF) as well as the accompanying
<a href="https://pathme.scai.fraunhofer.de/">PathMe web application</a> in
<a href="https://bmcbioinformatics.biomedcentral.com/articles/10.1186/s12859-019-2863-9"><em>BMC Bioinformatics</em></a>.</p>

<p><strong>July 29th, 2019</strong> At some point in 2019, the website (belframework.org)
hosting the BEL resources necessary to all BEL files was allowed to expire (I
think the transMart Foundation was paying for it and William was responsible,
but I’m not sure). With this abandonment, previously written BEL files could no
longer be compiled without a new resources server being deployed and the BEL
files updated. Luckily, the website was being built from a repository on the
OpenBEL GitHub organization, so only the files needed to be updated. The
responsibility of maitenance of the Selventa Large Corpus and Selventa Small
Corpus (previous released by Selventa under the CC-BY-3.0 license) was taken by
Charles Tapley Hoyt (Fraunhofer) and moved to
<a href="https://github.com/cthoyt/selventa-knowledge/">a new GitHub repository</a>.</p>

<p><strong>August 5th, 2019</strong> Farah Humayun (University of Bonn) and Daniel
Domingo-Fernández (Fraunhofer) make the
<a href="https://github.com/hemekg/hemekg/commit/7ca6df5088705f144048bff3a4987ab7644bfae3">first commit</a>
to the <a href="https://github.com/hemekg/hemekg">Heme Knowledge Graph (HemeKG)</a>. It is
later published in
<a href="https://doi.org/10.3389/fbioe.2020.00074"><em>Frontiers in Bioengineering and Biotechnology</em></a>.</p>

<p><strong>September 24, 2019</strong> Charles Tapley Hoyt (Fraunhofer)
<a href="https://medium.com/bel-news/bel-v2-2-enhancements-cfb4b27b22cb">announces</a> the
release of the BEL v2.2 specification on behalf of the BEL Language Committee.</p>

<p><strong>December 9th, 2019</strong> After a large curation project around neurodegenerative
diseases and tauopathies lead by Stephan Gebel and Charles Tapley Hoyt,
Fraunhofer makes its
<a href="https://github.com/pharmacome/conib/commit/98fe95b4c2dab202b313bf911257d4b009ce4440">last public commit</a>
to the
<a href="https://github.com/pharmacome/conib">Curation of Neurodegeneration in BEL (CONIB)</a>
project before the the departure of Charles Tapley Hoyt following his PhD and
interest in public curation in this project dwindled.</p>

<h3 id="2020">2020</h3>

<p><strong>March 4th, 2020</strong> The <a href="https://github.com/hetio/hetionet/">Hetionet project</a>
adopts BEL as a
<a href="https://github.com/hetio/hetionet#derivative-networks">distribution format</a> for
its integrative network suited for drug repositioning and target prioritization.</p>

<p><strong>March 4th, 2020</strong> The
<a href="https://github.com/OpenBioLink/OpenBioLink">OpenBioLink project</a> adopts BEL as
a
<a href="https://github.com/OpenBioLink/OpenBioLink#biological-expression-language-bel-writer">distribution format</a>
for benchmarking link prediction tasks on biological networks.</p>

<p><strong>April 11th, 2020</strong> Daniel Domingo-Fernández (Fraunhofer) releases the
<a href="https://github.com/covid19kg/">COVID19 disease map</a> along with a pre-print on
<a href="https://doi.org/10.1101/2020.04.14.040667"><em>bioRxiv</em></a>. Additionally, the paper
serves as a reference for Fraunhofer’s new
<a href="http://graphstore.scai.fraunhofer.de">OrientDB instance</a> that holds BEL and
their
<a href="https://bikmi.covid19-knowledgespace.de/">Biomedical Knowledge Miner web application</a></p>

<p><strong>April 3rd, 2020</strong> Jeremy Zucker (Pacific Northwest National Labs) and students
of Robert Ness/Olga Vitek (Northeastern; my alma mater!) begin developing
<a href="https://github.com/COVID-19-Causal-Reasoning/bel2pyro">pipelines</a> for
generating causal models (SCMs) from BEL graphs for modeling of COVID-19,
resulting in further interest in BEL in the
<a href="https://www.coronawhy.org/">CoronaWhy</a> working group.</p>

<p><strong>May 9th, 2020</strong> The OpenBEL community website,
<a href="https://openbel.org">https://openbel.org</a> is taken down.</p>

<hr />

<p>There are a few things that I would like to mention as afterthoughts that I
don’t know where to place on the timeline.</p>

<p>One of the most egregious ommisions I have made is the date of the BEL 2.0
release and the events that lead up to it. Even crazier, I don’t know much of
the pre-2016 history of how my group at Fraunhofer got involved with Selventa -
perhaps it was their long history of text mining (since the dictionary and CRF
days) that got these groups together. If someone has that information, I would
be really glad to include it here.</p>

<p>During my time at Fraunhofer, there were a lot of people working with BEL. Many
of them made tools and algorithms that never got published, so unfortunately,
they are not included in this history.</p>

<p>The Chemotoxicogenomics Database had been converted to BEL by Thomas Weigers a
long time ago, when the XML BEL format existed (another thing that I think
wasn’t worth bringing). I corresponded with him about it when I was at
Fraunhofer, but unfortunately don’t have access my the emails anymore to double
check exactly what we talked about. He did send me the database as XBEL which he
said he made with a script he wrote but didn’t have anymore. Ultimately, I
decided to re-write the converter to play nicer in the Bio2BEL ecosystem, which
worked for a while and then broke because its downstream dependency for parsing
the database wasn’t updated. I never got around to re-writing this <em>again</em>.</p>

<p>I’m not sure what happened at PatientsLikeMe, but from LinkedIn I can tell that
there was a mass migration of ex-Selventa to
<a href="https://www.quartz.bio/">Quartz Bio</a>, who also appear to be
<a href="https://www.linkedin.com/jobs/view/knowledge-management-expert-quartzbio-at-precision-medicine-group-1714058313/">hiring BEL people</a>
to join their team. After burning out from finishing my PhD in late 2019, I
haven’t been proactive about keeping up with many people (quarantine life isn’t
making me feel that kind of motivation either, which is somehow categorically
different from the entire day I spent researching and preparing this blog post),
though I do personally know some of them and could ask…</p>

<p>One of the other peculiarities about the history of BEL is the adjacent history
of BioPAX. This happened <em>way</em> before my time, so I wonder why they diverged so
much. I think that they’re ultimately trying to accomplish the same thing, which
is to be a place for putting structured information (and modeling, to an
extent). It’s most definitely the case that BioPAX has achieved greater
popularity and penetration, due to (in my opinion) the excellent labs that are
backing the standard and the fire hydrant of <em>high impact papers</em> coming out of
them. However, I think this success might be holding BioPAX back, because many
of these papers are simply pulling content from Pathway Commons to do gene set
enrichment analysis. Now, if you’re working adjacent to an oncology unit in a
hospital, then this is all you need to do to get results good results, write
high impact papers, and ultimately help patients. Don’t get me wrong - I’m much
happier to see papers that achieve their scientific goals using simple methods.
I just think there’s much more potential for BioPAX. I think we’re seeing that
movement happen for BEL already.</p>

<p>Then there’s that idea of converting between BioPAX and BEL. It’s sort of a
non-starter, since all BioPAX is encoded differently - just because data is able
to be stored in a given format (e.g., ontology deriving from BioPAX, RDF, etc.),
it doesn’t necessarily mean that the content can be put together with other
content in the same format in a meaningful way.</p>

<p>As I close, I will again acknowledge my biases. I’m quite proud of the work I
did during my master’s and doctoral work at Fraunhofer. I’m thankful for all the
people who were interested in my projects, contributed to them, and then joined
me as co-authors on my publications. When it came to writing this history, I was
in a situation where I had lots of high granular information to share on the
things that I worked on and also the the desire to share as much of it as
possible. I hope I did a good enough job at laying out the landscape of the
other things going on outside my perspective.</p>

<p>If you’ve got something to add, all of my contact information is available on
the footer of my blog. Or make a
<a href="https://github.com/cthoyt/cthoyt.github.io/edit/master/_posts/2020-04-28-history-of-bel.md">pull request against this page</a>
directly. Special thanks to Keith Ellison for suggesting
<a href="https://github.com/cthoyt/cthoyt.github.io/pull/10">these changes</a>.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="BEL" /><summary type="html"><![CDATA[The company and community that surround the Biological Expression Language (BEL) are enigmatic, to say the least. This post represents the best I could do to tell the history of Selventa and BEL.]]></summary></entry><entry><title type="html">How to Code with Me - Flake8 Hell</title><link href="https://cthoyt.com/2020/04/25/how-to-code-with-me-flake8.html" rel="alternate" type="text/html" title="How to Code with Me - Flake8 Hell" /><published>2020-04-25T08:00:00+00:00</published><updated>2020-04-25T08:00:00+00:00</updated><id>https://cthoyt.com/2020/04/25/how-to-code-with-me-flake8</id><content type="html" xml:base="https://cthoyt.com/2020/04/25/how-to-code-with-me-flake8.html"><![CDATA[<p>As scientists, we place huge importance on the communication of our results. We
spend lots of time on editing, revising, and formatting so people can understand
what we did. We also write a lot of code, so why aren’t we investing the same
amount of love? Enter, <a href="https://flake8.pycqa.org/en/latest/">flake8</a>.</p>

<p>It’s incredibly important that we write following community standards so when
other people read our work, they don’t have to think about how it’s organized.
For scientific prose, this usually means the IMRD
(introduction-methods-results-discussion) format. In Python, my current favorite
programming language for science, this means using a standardized number of
spaces for indents (4), using triple-double quotes for docstrings in the
beginning of each module, class, and function, and lots more.</p>

<p>It’s pretty intimidating to figure out style. For english prose, Strunk and
White wrote
<a href="http://www.jlakes.org/ch/web/The-elements-of-style.pdf"><em>The Elements of Style</em></a>.
For Python, Guido van Rossum wrote
<a href="https://www.python.org/dev/peps/pep-0008/">PEP-8</a> and Raymond Hettinger
presented <a href="https://www.youtube.com/watch?v=wf-BqAjZb8M">Beyond PEP-8</a>. Even with
these resources, it’s still hard to learn which are rules and which
<a href="https://www.youtube.com/watch?v=k9ojK9Q_ARE">are more like guidelines</a>.</p>

<p>This post is a short explanation of how I use <code class="language-plaintext highlighter-rouge">flake8</code> to keep a consistent
style in the code in my Python projects. There’s a similar command line tool for
fixing the style in R projects that’s already built into most operating
systems - <code class="language-plaintext highlighter-rouge">rm -rf *</code>, but I won’t get more into that here.</p>

<p>It’s pretty easy to get up and running with <code class="language-plaintext highlighter-rouge">flake8</code> - just run
<code class="language-plaintext highlighter-rouge">pip install flake8</code> then use it from the shell on a python file like
<code class="language-plaintext highlighter-rouge">flake8 my_file.py</code> or <code class="language-plaintext highlighter-rouge">flake8 my_directory/</code>. Then, it outputs a list of
problems that need to be fixed on a line-by-line basis in your code.</p>

<p><img src="/img/flake8_output.png" alt="Flake8 Feedback" /></p>

<p>You can also install plugins with <code class="language-plaintext highlighter-rouge">pip</code> like that extend the kinds of things it
checks. A few that I install are:</p>

<ul>
  <li><a href="https://github.com/gforcada/flake8-builtins">flake8-builtins</a> - make sure you
don’t accidentally name a variable the same thing as a builtin. This happens a
lot with <code class="language-plaintext highlighter-rouge">id</code>.</li>
  <li><a href="https://github.com/PyCQA/flake8-bugbear">flake8-bugbear</a> - “find likely bugs
and design problems in your program”, like when you have an unused variable in
a loop</li>
  <li><a href="https://github.com/and3rson/flake8-colors">flake8-colors</a> - add color to the
<code class="language-plaintext highlighter-rouge">flake8</code> output (explanation how to set up is below)</li>
  <li><a href="https://github.com/PyCQA/flake8-commas">flake8-commas</a> - add trailing commas
where appropriate</li>
  <li><a href="https://github.com/adamchainz/flake8-comprehensions">flake8-comprehensions</a>
reminders to use list comprehensions where appropriate</li>
  <li><a href="https://github.com/PyCQA/flake8-docstrings">flake8-docstrings</a> - make sure
your docstrings are present and written in the right format</li>
  <li><a href="https://github.com/PyCQA/flake8-import-order">flake8-import-order</a> - make
sure your imports are organized properly</li>
  <li><a href="https://github.com/JBKahn/flake8-print">flake8-print</a> - make sure you never
ever ever use <code class="language-plaintext highlighter-rouge">print()</code>. The literal only exception is when using print to get
text into a file with <code class="language-plaintext highlighter-rouge">print(..., file=...)</code></li>
  <li><a href="https://github.com/MichaelKim0407/flake8-use-fstring">flake8-use-fstring</a> -
make sure you’re using f-strings instead of <code class="language-plaintext highlighter-rouge">%</code> or <code class="language-plaintext highlighter-rouge">.format()</code> formatting.
Exception being for logging.</li>
  <li><a href="https://github.com/PyCQA/pep8-naming">pep8-naming</a> - make sure names of
variables, classes, and modules look right.</li>
  <li><a href="https://github.com/PyCQA/pydocstyle/">pydocstyle</a> - docstring style checker</li>
</ul>

<p>In each of my repositories, I put all of the information on how to install
<code class="language-plaintext highlighter-rouge">flake8</code> and its plugins then run them in a <code class="language-plaintext highlighter-rouge">tox</code> configuration under the
<code class="language-plaintext highlighter-rouge">[testenv:flake8]</code> header so they can easily reproducibly run with
<code class="language-plaintext highlighter-rouge">tox -e flake8</code>. An example of part of one of my <code class="language-plaintext highlighter-rouge">tox.ini</code> files (which always
lives in the root of the repository) is below:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[testenv:flake8]</span>
<span class="py">skip_install</span> <span class="p">=</span> <span class="s">true</span>
<span class="py">deps</span> <span class="p">=</span>
    <span class="err">flake8</span>
    <span class="err">flake8-bandit</span>
    <span class="err">flake8-builtins</span>
    <span class="err">flake8-bugbear</span>
    <span class="err">flake8-colors</span>
    <span class="err">flake8-commas</span>
    <span class="err">flake8-comprehensions</span>
    <span class="err">flake8-docstrings</span>
    <span class="err">flake8-import-order</span>
    <span class="err">flake8-print</span>
    <span class="err">flake8-use-fstring</span>
    <span class="err">pep8-naming</span>
    <span class="err">pydocstyle</span>
<span class="py">commands</span> <span class="p">=</span>
    <span class="err">flake8</span> <span class="err">src/pybel/</span> <span class="err">tests/</span> <span class="err">setup.py</span>
<span class="py">description</span> <span class="p">=</span> <span class="s">Run the flake8 tool with several plugins (bandit, docstrings, import order, pep8 naming).</span>
</code></pre></div></div>

<p>Another configuration file you can set up in the root of the repository is
<code class="language-plaintext highlighter-rouge">.flake8</code>. Unfortunately, the Python configuration file reader doesn’t allow
some of the crazy characters that I want to use for the colors so this can’t be
included in <code class="language-plaintext highlighter-rouge">setup.cfg</code> or <code class="language-plaintext highlighter-rouge">tox.ini</code> like most of your other configuration.</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[flake8]</span>
<span class="py">ignore</span> <span class="p">=</span>
    <span class="c"># line break before binary operator
</span>    <span class="err">W503</span>
<span class="py">exclude</span> <span class="p">=</span>
    <span class="err">.tox,</span>
    <span class="err">.git,</span>
    <span class="err">__pycache__,</span>
    <span class="err">docs/source/conf.py,</span>
    <span class="err">build,</span>
    <span class="err">dist,</span>
    <span class="err">tests/fixtures/*,</span>
    <span class="err">*.pyc,</span>
    <span class="err">*.egg-info,</span>
    <span class="err">.cache,</span>
    <span class="err">.eggs</span>
<span class="py">max-line-length</span> <span class="p">=</span> <span class="s">120</span>
<span class="py">import-order-style</span> <span class="p">=</span> <span class="s">pycharm</span>
<span class="py">application-import-names</span> <span class="p">=</span>
    <span class="err">pybel</span>
    <span class="err">bel_resources</span>
    <span class="err">tests</span>
<span class="py">format</span> <span class="p">=</span> <span class="s">${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s</span>
</code></pre></div></div>

<p>First thing you’ll notice is the <code class="language-plaintext highlighter-rouge">ignore</code> list. This isn’t here to turn <code class="language-plaintext highlighter-rouge">flake8</code>
off because you’re feeling lazy. If somebody includes a change in this list in
their PR, you have to explain to them that compliance is not optional, then help
them work through the problem that they obviously gave up on solving. It’s
actually there for you, as the project maintainer, to enumerate the <code class="language-plaintext highlighter-rouge">flake8</code>
rules that you don’t agree with. For example, I totally disagree with the <code class="language-plaintext highlighter-rouge">W503</code>
line break before operator rule. I want to write long conditionals with and
statements on the first line, like this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span>
   <span class="n">condition_1</span>
   <span class="ow">and</span> <span class="n">condition_2</span>
   <span class="ow">and</span> <span class="n">condition_3</span>
<span class="p">):</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'all true'</span><span class="p">)</span>
</code></pre></div></div>

<p>One of the benefits of this style is you can add more lines with only single
line diffs. The other is that the reader always sees the operation that goes
with each line. Same could be done with arithmatic that could incorporate not
only <code class="language-plaintext highlighter-rouge">+</code> but also <code class="language-plaintext highlighter-rouge">-</code>.</p>

<p>Next is the <code class="language-plaintext highlighter-rouge">exclude</code> block. Just copy/paste this each time, since it has lots
of garbage you don’t want <code class="language-plaintext highlighter-rouge">flake8</code> to bother with. One of the checkers in
<code class="language-plaintext highlighter-rouge">flake8</code> is for function “cyclomatic” complexity. You can make the maximum
number higher with <code class="language-plaintext highlighter-rouge">max-complexity</code>. Normally, you want this to be enforced, but
sometimes there’s no way around a complex function. For this, you can add a code
comment <code class="language-plaintext highlighter-rouge">noqa</code> followed by the error code like <code class="language-plaintext highlighter-rouge"># noqa:W123</code>. Again, adding tags
to ignore bad style just to pass <code class="language-plaintext highlighter-rouge">flake8</code> is against the point.</p>

<p>The <code class="language-plaintext highlighter-rouge">max-line-length</code> is a very contentious setting. I think 120 is fine. Some
people think 78, 79, or 80 is best because of the standard sizes of old computer
screens or punch cards… When I get older and I can’t read my computer screen,
I’ll probably make the text bigger and change my mind about this. If you find
yourself breaking up lines in a totally non-sensical, unstyled way, then you’re
conforming too tightly to the rules. Sorry about the mixed messages!</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import-order-style = pycharm
application-import-names =
    pybel
    bel_resources
    tests
</code></pre></div></div>

<p>I copied this again because this part is really important. You have to tell
<code class="language-plaintext highlighter-rouge">flake8</code> what rules you use for import order. I use the pycharm rules, which
group python builtin packages, then 3rd party packages, then my packages. The
<code class="language-plaintext highlighter-rouge">application-import-names</code> is a place to list what are your packages.</p>

<p>Last is the <code class="language-plaintext highlighter-rouge">format</code> entry, which gives the nice colorful output. Copy paste
this! I borrowed mine from <a href="https://github.com/scolby33">Scott Colby</a>.</p>

<hr />

<p>After all of that, I set up Travis CI to run <code class="language-plaintext highlighter-rouge">tox</code> every time code is pushed to
the repository. If you’re working in a team, you probably do something like the
fork/pull request or branch/pull request workflow on GitHub to support doing
code review before merging new code. The best part is that there’s a big box on
each pull request that checks if <code class="language-plaintext highlighter-rouge">flake8</code> passed (among other tests), which
means that there were no errors detected.</p>

<p>I encourage my teammates to make pull requests as soon as they start working on
code. GitHub even has a “draft pull request” mode now. However, before asking
anyone to review your code, it has to pass <code class="language-plaintext highlighter-rouge">flake8</code>. And obviously, no code that
isn’t passing flake8 can be merged.</p>

<p>This is a <em>very</em> painful process to get people used to. I’ve done it with many
groups of people and always got pushback. However, everyone who has gone through
the process with me has come out the other side happy that they did it. It’s
important that when you start enforcing coding rules on other people that you
are a resource for them - when somebody is frustrated by a flake8 error code
they have never seen, they will likely forget how to use Google. They will
probably ask you for help. You have to resist the urge to send
<a href="https://lmgtfy.com">lmgtfy</a> links to them and be patient. Because eventually,
they will do it on their own, and spread the gospel of <code class="language-plaintext highlighter-rouge">flake8</code>.</p>

<p>While a good arsenal of <code class="language-plaintext highlighter-rouge">flake8</code> plugins provides a solid foundation, it’s not
all that needs to be done to make your code readable and look good. Just like
with reading and speaking, the best way to develop a sense of style is by
reading <em>lots</em> of code (with the caveat that reading poorly written code
probably won’t teach you much). Within the rules imposed by <code class="language-plaintext highlighter-rouge">flake8</code>, there is
lots of space for style. If you watch lectures from David Beazley, you’ll notice
a very different style from Raymond Hettinger, and also from me.</p>

<p>Now that you’ve made it to the end of this short guide, I wish you the best of
luck on developing your own style!</p>

<hr />

<p>Are you working with people who are particularly unsusceptible to Travis CI
emails or checking the big red box on pull requests? You could try getting them
set up with <a href="https://pre-commit.com/">pre-commit hooks</a>, which run the style
checks locally any time they try and push (even if it’s to a branch) and it will
give them the message in the console.</p>

<p>Is style not your thing at all / you’re not ready to let go of your identity as
a Java/Perl developer? Maybe consider <a href="https://github.com/psf/black">Black</a>,
which actually re-writes your code in a deterministic style. I don’t live by it,
but it’s a great tool to run on a code base that’s never been loved before going
back and stylizing it.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="code with me" /><summary type="html"><![CDATA[As scientists, we place huge importance on the communication of our results. We spend lots of time on editing, revising, and formatting so people can understand what we did. We also write a lot of code, so why aren’t we investing the same amount of love? Enter, flake8.]]></summary></entry><entry><title type="html">Inspector Javert’s Xref Database</title><link href="https://cthoyt.com/2020/04/19/inspector-javerts-xref-database.html" rel="alternate" type="text/html" title="Inspector Javert’s Xref Database" /><published>2020-04-19T08:00:00+00:00</published><updated>2020-04-19T08:00:00+00:00</updated><id>https://cthoyt.com/2020/04/19/inspector-javerts-xref-database</id><content type="html" xml:base="https://cthoyt.com/2020/04/19/inspector-javerts-xref-database.html"><![CDATA[<p>On top the issue of <a href="/2020/04/18/ooh-na-na.html">resolving identifiers to their
names</a>, the bioinformatics community has a
hard time figuring out when two identifiers from different databases are
equivalent. You know who else has the same problem? Inspector Javert. Get ready
for a <em>Les Miserables</em>-themed post on how to address this long-standing problem.</p>

<p>I have to start my tale of woes by disclosing my source material. I loved both
the 1985 and 1987 recordings from the respective original London and Broadway
casts. But, for the purposes of this post, I will assume that you’ve seen the
excellent 2012 film adaptation of Alain Boublil, Jean-Marc Natel, and Herbert
Kretzmer’s musical adaptation of Victor Hugo’s novel <em>Les Miserables</em> and tell
the story through that perspective. I also want to you to know that I enjoyed
Russell Crowe’s Inspector Javert very much.</p>

<p><em>Les Miserables</em> begins with the Work Song, in which the protagonist,
<code class="language-plaintext highlighter-rouge">prisoner:24601</code> is confronted by Inspector Javert while doing some… work. He
insists he has a name, Jean Valjean and his identifier in his
<a href="https://en.wikipedia.org/wiki/Faverolles,_Aisne">home village</a>’s fictional
database (that I just retconned) was <code class="language-plaintext highlighter-rouge">faverolles:2468</code>. Javert isn’t interested
in his name. It’s enough that he has a cross-reference between <code class="language-plaintext highlighter-rouge">faverolles:2468</code>
is equivalent to <code class="language-plaintext highlighter-rouge">prisoner:24601</code>. He was only there to inform Jean Valjean that
his parole has begun and issues him a <em>passeport jaune</em> (yellow ticket) for the
commune of <a href="https://en.wikipedia.org/wiki/Pontarlier">Pontarlier</a>.</p>

<p>I’m sure this passport also had an identifier on it. I’m going to take a bit of
creative freedom and say it was <code class="language-plaintext highlighter-rouge">pontarlier:25791</code>. It probably also had Jean
Valjean’s prisoner number on it so everybody knew he was in the 1800’s fictional
French convict database. The fictional 1800’s French took maintaining
cross-references very seriously.</p>

<p>Jean Valjean never made it to Pontarlier. Instead, he broke his parole, forged
some new documents, and went to Montreuil-sur-Mer under the new name of Monsieur
Madeleine. It’s probably the case that his identifier for the Montreuil-sur-Mer
city database was <code class="language-plaintext highlighter-rouge">montreuil-sur-mer:1357</code>, or something like this (more
retcons!). It must have been a good fake, because even the king of France
recognized him (note: this plot point did not appear in the film).</p>

<p>Javert figured out Jean Valjean broke his parole basically immediately and set
out on his quest to find and capture <code class="language-plaintext highlighter-rouge">prisoner:24601</code> once again. Until this
point, Javert has access to the prisoner registry and yellow tickets. He knows
<code class="language-plaintext highlighter-rouge">prisoner:24601</code> is the same as both <code class="language-plaintext highlighter-rouge">faverolles:2468</code> and <code class="language-plaintext highlighter-rouge">pontarlier:25791</code>.</p>

<p>The part that will hit close to home for many bioinformaticians is that when
Javert goes to Montreuil-sur-Mer, he meets Monsieur Madeleine. He is unaware
that it is Jean Valjean. There is no cross-reference between <code class="language-plaintext highlighter-rouge">faverolles:2468</code>
and <code class="language-plaintext highlighter-rouge">pontarlier:25791</code> or <code class="language-plaintext highlighter-rouge">montreuil-sur-mer:1357</code>. If there were a
cross-reference in the fictional French 1800’s inspector database, Javert could
have arrested Jean Valjean on sight. Instead, Javert had to the hard work of
curating cross-references himself and finding out who was the same in the
<code class="language-plaintext highlighter-rouge">montreuil-sur-mer</code> database as <code class="language-plaintext highlighter-rouge">prisoner:24601</code>. Admittedly, he probably would
have called this <em>inspecting</em>.</p>

<p>The next part that will hit even closer to home for many bioinformaticians is
that after his inspecting, Javert actually identified the wrong guy! This lead
to one of the my favorite songs in musical theater ever
(<a href="https://www.youtube.com/watch?v=izuD30Cp5Ao">Who Am I?</a>), where Monsieur
Madeleine (<code class="language-plaintext highlighter-rouge">montreuil-sur-mer:1357</code>, also actually Jean Valjean, but Javert
didn’t yet realize this) admits that he is actually <code class="language-plaintext highlighter-rouge">prisoner:24601</code>. In this
extended metaphor of a blog post, Jean Valjean’s confession in “Who Am I” is
effectively the same as a database providing its own cross-references to other
database. Would be nice if everyone did this, and did it well, huh?</p>

<p>You should know that Javert is a powerful cross-reference reasoning machine. He
already knew <code class="language-plaintext highlighter-rouge">faverolles:2468</code> was the same as <code class="language-plaintext highlighter-rouge">prisoner:24601</code>. Now he knew
that <code class="language-plaintext highlighter-rouge">montreuil-sur-mer:1357</code> was the same as <code class="language-plaintext highlighter-rouge">prisoner:24601</code>. This way, he
could infer that <code class="language-plaintext highlighter-rouge">montreuil-sur-mer:1357</code> (Monsieur Madeleine) is actually
<code class="language-plaintext highlighter-rouge">faverolles:2468</code> (Jean Valjean). One of the nice properties of cross-references
is that they’re transitive through any number of connections. We’ll take
advantage of this fact later. You’ll also have to excuse the fact that
throughout this post, I’m operating under the assumption that “cross-references”
and “equivalences” are the same thing. That’s not always true, and sometimes it
can even get you in trouble. For example, provenance can be a cross-reference,
disease-gene associations are considererd as cross-references in MONDO (I
think), and OBO even gives specific semantics for when you should consider this
assumption valid. We’ll just have to live with it for now.</p>

<p>Javert might have got lucky that Jean Valjean revealed himself once, but the
show must go on! Jean Valjean had many more songs to sing and thus had to escape
from Montreuil-sur-Mer to Paris. This meant that Javert has to find <em>another</em>
mapping to Jean Valjean’s new <code class="language-plaintext highlighter-rouge">paris</code> identifier. And we already know that the
French 1800’s inspector database of cross-references was not being maintained.
Exhausting!</p>

<hr />

<p>In the bioinformatics community, we have a very similar problem to Inspector
Javert. There are lots of databases that are talking about the same things, but
only a few of them provide mappings between each other. This means that we
either have to curate our own cross-references, do our best to infer new
cross-references based on ones we already have, or throw our hands in the air.</p>

<p>Luckily, we have a few standardized resources to fall back on. In addition to
standardizing the storage of identifier/name pairs, the OBO format standardizes
the way cross-references are stored and the OBO Foundry already contains quite a
few cross-references imported from the ontologies that it covers.</p>

<p>One of the most difficult entity types to map from database to database are
phenotypes because of the variety of language used to describe each, the
differences in semantics of how each is defined, and the sheer number of
databases. Unfortunately, some of the most popular like MeSH and to an extent,
UMLS, NCIT, SNOMED-CT, and ICD (seemingly the culprits are mostly American!?)
provide very little accessible information. Some are even paid, so the ony
cross-references that exist are externally curated ones from other laudable
sources like HP, DOID, and EFO. In fact, dealing with phenotypes is such a pain,
that there is a project called the
<a href="https://monarchinitiative.org/">Monarch Initiative</a> that has a huge staff
trying to solve exactly this problem and publish the results through the
<a href="https://github.com/monarch-initiative/mondo">Monarch Disease Ontology (MONDO)</a>.
Normally, I would reference
<a href="https://xkcd.com/927/">this XKCD comic about making new standards</a> when hearing
about something like this. But these are dire times, and one of my opinions is
that you should always trust curators who love what they do.</p>

<p>There are also lots of cross-references available from databases that don’t
maintain their nomenclature as an ontology. One example is
<a href="https://downloads.thebiogrid.org/File/BioGRID/Latest-Release/BIOGRID-IDENTIFIERS-LATEST.tab.zip">BioGRID</a>,
which assigns proteins internal accession numbers, but almost all of them
cross-reference out to Entrez Gene (I counted less than 15 that didn’t, and 3 of
them are COVID-related, so cut them some slack). As an aside, I don’t really
understand why BioGRID would go through the effort of maintaining their own
accession numbers. In the literal handful of cases where they can’t reference
Entrez Gene, I think it would be better to email the maintainers and work with
them to make improvements.</p>

<p>It’s also worth noting that excellent resources like HGNC, MGI, RGD, SGD,
Ensembl, UniProt, and others in the genome (and gene product) nomenclature do a
stellar job at maintaining cross-references. So to all of the curators and
maintainers who work there, I would like to sincerely thank you.</p>

<p>There are also community-curated cross-references sources. One of the notable
ones is from Harvard Medical School, that’s mapping MeSH identifiers to gene
identifiers in the
<a href="https://raw.githubusercontent.com/indralab/gilda/master/gilda/resources/mesh_mappings.tsv">Gilda GitHub repository</a>.
I think this is really a good time to point out that MeSH contains a bit of
everything, is ubiquitous throughout the bioinformatics community, and in my
opnion is is doing a huge disservice by not providing these kinds of mappings
itself. Or, alternatively, it is, and both the Harvard guys and I have never
found it. It’s not impossible, but we’re all very motivated, so I think we would
have found if it did. If any MeSH maintainers are reading this and want help
making this happen, I would be elated to donate my time to you to help solve
this problem.</p>

<p>With all these data source in mind, I built an extensible pipeline in
<a href="https://github.com/pyobo/pyobo/blob/master/src/pyobo/xrefdb/xrefs_pipeline.py">PyOBO</a>
for extracting cross-references from entries in OBO Foundry and other
cross-reference sources. Throughout the process, I realized that these sources
have an incredible variety in how they name prefixes and how the OBO format
itself has been (ab)used. I made lots of improvements, wrote extensible code
that allowed the specification of new rules through external files (and thus
less code writing in the future), and did lots more curation. I won’t get into
the technical part of that here, since you can read the code (if you dare).</p>

<p>After all that this coding, I wrote a script (just run <code class="language-plaintext highlighter-rouge">obo javerts-xrefs</code>) that
takes all available cross-references, normalizes their namespaces, normalizes
their identifiers, and dumps them in a big ‘ol TSV file. 5 columns - source
namespace, source identifier, target namespace, target identifier, and
provenance (ontology name or URL). No nonsense. Get it at
<a href="https://doi.org/10.5281/zenodo.3757266"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.3757266.svg" alt="DOI" /></a>.
I’ll make updates periodically as I add new sources.</p>

<hr />

<p>Once you have a database of cross-references, you have actually built an
undirected graph. Equivalences go both ways, and they are transitive. This means
that every connected component in an equivalence graph represents a set of
entities that are mutually equivalent. In other words, if a path exists between
two nodes in an equivalent graph, then they are equivalent.</p>

<p>Even better, you don’t have to materialize all of the possible inferred
equivalences when you have an equivalence graph because identifying all of the
nodes in a connected component can be done in linear time with respect to the
size of the connected component, which is usually pretty small, by using a
breadth-first or depth-first search.</p>

<p>Based off of that, one application of an equivalence graph is to identify all of
the nodes that are equivalent to a given node. You can also get a little tricker
and identify the paths through which the traversal must go if you want to
establish an equivalency. You could even go further and weight edges based on
how much you trust the source from which they came to identify how much you
should believe in a mapping. For example, if you have a percent confidence in
each mapping being right, then the confidence in the whole pathway would be the
product of the confidences.</p>

<p>The actual problem I set out to solve was given a set of entities, remap all of
them based on a prioritized list. For example, I might have a set of entities
that contains HGNC genes, Entrez Genes, and OMIM genes. If my favorite
nomenclature consortium is Entrez, my second favorite is HGNC, and my third
favorite is OMIM and I have an equivalence database, I might want to remap all
of my identifiers. This is very important during the curation of mechanistic
biology (such as with BEL), since curators will likely use all sorts of
identifiers with no clear guidelines or rules. This means that the same entity
might appear twice with different identifiers in the same curated data!</p>

<p>Given a priority list, you can even transform an equivalence graph into a
directed graph where each identifier has a single out edge pointing towards the
identifier that is the best mapping. Then, each connected component would become
a star graph. There’s actually a better data structure for this, since each
entity points to exactly one thing - a mapping. This is a more efficient data
structure for storage, and if your graph is implemented as an adjacency
dictionary (becuase you’re using <code class="language-plaintext highlighter-rouge">networkx</code>, right?), then you basically already
have this.</p>

<p>I’ve provided an implementation for all of these in PyOBO. They can be run as a
web API with <code class="language-plaintext highlighter-rouge">python -m pyobo.apps.mapper</code>. There’s a keyword argument to allow
you to load the TSV from Inspector Javert’s Xref Database directly, or if you’re
feeling lucky, to regenerate it yourself. Below I will give a few examples of
how to use it. Later, I would also like to host this service for anyone to use.</p>

<ol>
  <li>Install PyOBO with <code class="language-plaintext highlighter-rouge">pip install git+https://github.com/pyobo/pyobo.git</code></li>
  <li>Download Inspector Javert’s Xref Database from Zenodo, unpack it, and find
the xrefs file.</li>
  <li>Run the web service with
<code class="language-plaintext highlighter-rouge">python -m pyobo.apps.mapper -x inspector_javerts_xrefs.tsv.gz</code></li>
  <li>Use the following code to figure stuff out!</li>
</ol>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">requests</span>

<span class="c1"># Get all entities mapped to MAPT, including through chains of xrefs
</span><span class="n">successful_request</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://localhost:5000/mappings/hgnc:6893'</span><span class="p">).</span><span class="n">json</span><span class="p">()</span>
<span class="s">"""
{
    "orphanet:123144": [
        {
            "provenance": "hgnc",
            "source": "hgnc:6893",
            "target": "orphanet:123144"
        }
    ],
    "pr:P10636": [
        {
            "provenance": "hgnc",
            "source": "hgnc:6893",
            "target": "uniprot:P10636"
        },
        {
            "provenance": "pr",
            "source": "uniprot:P10636",
            "target": "pr:P10636"
        }
    ],
    ...
}
"""</span>

<span class="c1"># Keep in mind this isn't a validation service
</span><span class="n">unsuccessful_request</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://localhost:5000/mappings/hgnc:0000'</span><span class="p">).</span><span class="n">json</span><span class="p">()</span>
<span class="c1"># {"message": "could not find curie", "query": {"curie": "hgnc:0000"}, "success": False}
</span>
<span class="c1"># Get all paths mapping MAPT in HGNC to Ensembl. Returns a list of paths (which are lists of xrefs)
</span><span class="n">path_request</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://localhost:5000/mappings/hgnc:6893/ensembl:ENSG00000186868'</span><span class="p">).</span><span class="n">json</span><span class="p">()</span>
<span class="s">"""
[
    [
        {
            "provenance": "hgnc",
            "source": "hgnc:6893",
            "target": "ensembl:ENSG00000186868"
        }
    ]
]
"""</span>

<span class="c1"># Get the priority identifier for MAPT identified by Ensembl
</span><span class="n">prioritize_request</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://localhost:5000/prioritize/cosmic:MAPT'</span><span class="p">).</span><span class="n">json</span><span class="p">()</span>
<span class="c1"># {"found": True, "query": "cosmic:MAPT", "result": "hgnc:6893"}
</span>
<span class="c1"># What happens when a CURIE can't be found for prioritization
</span><span class="n">unsuccessful_prioritize_request</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://localhost:5000/prioritize/cosmic:NOPE'</span><span class="p">).</span><span class="n">json</span><span class="p">()</span>
<span class="c1"># {"found": False, "query": "cosmic:NOPE"}
</span></code></pre></div></div>

<p>I’d like to give a big thanks to my high school music teacher, Ken Tedeschi, for
helping me (and basically everyone else) fall in love with Les Mis in high
school. Writing about my work was so much more fun in extended metaphor. I would
also like to thank Hugh Jackman. You know, for being Hugh Jackman.</p>

<hr />

<p>I have some random afterthoughts that I think might be worth including, that I’m
adding after originally posting this.</p>

<p>You might be wondering why I didn’t get into a discussion about the
<a href="https://www.ebi.ac.uk/about/news/announcement/industry-collaboration-ontology-mapping-service">Ontology Mapping Service (OXO)</a>
from the EBI. It looks to me like this project has been abandoned. Even if not,
it’s API has most of the same issues that I described in a <a href="/2020/04/18/ooh-na-na.html">previous
post</a>.</p>

<p>I’m also aware of <a href="https://bridgedb.github.io">BridgeDB</a>, from which I think I
will be able to take inspiration to include more xrefs later. However, I think
they’re limited in scope, and PyOBO is more about standardizing data so nobody
has to figure out databases… again and again and again.</p>

<p>One glaring omission from this work is WikiData mappings. I have a plan to
include curated information in the PyOBO metaregistry that links databases to
their WikiData properties. That will allow me to build an automated framework
for downloading these mappings, given the curation of the properties.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="mappings" /><summary type="html"><![CDATA[On top the issue of resolving identifiers to their names, the bioinformatics community has a hard time figuring out when two identifiers from different databases are equivalent. You know who else has the same problem? Inspector Javert. Get ready for a Les Miserables-themed post on how to address this long-standing problem.]]></summary></entry><entry><title type="html">Ooh Na Na, What’s My Name?</title><link href="https://cthoyt.com/2020/04/18/ooh-na-na.html" rel="alternate" type="text/html" title="Ooh Na Na, What’s My Name?" /><published>2020-04-18T08:00:00+00:00</published><updated>2020-04-18T08:00:00+00:00</updated><id>https://cthoyt.com/2020/04/18/ooh-na-na</id><content type="html" xml:base="https://cthoyt.com/2020/04/18/ooh-na-na.html"><![CDATA[<p>We have a big problem in the bioinformatics community with namespaces,
identifiers, and names. And nobody’s posed the question better than
<a href="https://www.youtube.com/watch?v=U0CGsw6h60k">Rihanna herself</a>.</p>

<p>During my Ph.D. at Fraunhofer, one of the old text miners reminisced to me about
the late 90’s and early naughties when they had to curate their own dictionaries
of synonyms for entities. I was lucky enough to have joined the bioinformatics
community after excellent nomenclature resources like
<a href="https://www.ebi.ac.uk/chebi/">ChEBI</a> and the <a href="https://www.genenames.org/">HGNC</a>
were established and accepted by the community as gospel.</p>

<p>I consider these sources excellent because it’s quite easy to get a list of the
identifiers and corresponding names that they maintain (TSV, etc.). There are
other nomenclatures, like the
<a href="ftp://ftp.expasy.org/databases/enzyme/enzyme.dat">ExPASy Enzyme Classes</a>, that
are stored as text files in non-standard formats.</p>

<p>The Open Biomedical Ontology (OBO) format and
<a href="http://www.obofoundry.org/">OBO Foundry</a> were first published in
<a href="https://www.nature.com/articles/nbt1346">2007</a> as a solution for standardizing
a growing set of biomedical ontologies that few shared semantics. Many ontology
maintainers adopted their format, or at least used the OWL to OBO converter
tools to include their ontologies in a reusable format. However, there remain
some notable holdouts like the
<a href="https://github.com/CLO-ontology">Cell Line Ontology</a> that have not begun to
distribute their content as OBO.</p>

<p>In parallel, the <a href="https://www.ebi.ac.uk/ols">Ontology Lookup Service (OLS)</a> was
published as one of many front-ends for exploring this growing list of
resources. In comparison, it may have been one of the first tools to provide a
nice user experience that included a search engine (powered by
<a href="http://www.obofoundry.org/">solr</a>, because they’re living in the Java world).</p>

<p>Both are lacking - there does not exist a solid OBO ecosystem (though Martin
Larralde’s <a href="https://github.com/althonos/pronto">pronto</a> may well soon change
that) and even worse, the content in OBO loosely follows the standard, at best.
On the other hand, the OLS has both an over-engineered interface that isn’t
quite user friendly. For example, if you want to look up programmed cell death
(GO:0012501), you have to know the internal OLS key for the namespace and the
PURL for the identifier, which is not so obvious. Then you can finally hit the
<a href="https://www.ebi.ac.uk/ols/api/ontologies/go/terms?iri=http://purl.obolibrary.org/obo/GO_0012501">API</a>.</p>

<p>And still, both of them lack some of my favorite, and arguably most important
namespaces, like HGNC, RGD, MGI, UniProt, Entrez Gene, and PubChem. As an aside,
dealing with PubChem is for people operating on a whole different level, so I’m
not blaming anyone for dropping the ball on that one. Later, I will confess to
doing the same.</p>

<p>Even worse, the OBO Foundry and OLS can’t even agree on what to call some
namespaces. A great example is the NCBI taxonomy database. On the NCBI site,
they say that the namespace is called <code class="language-plaintext highlighter-rouge">NCBI</code> and compact uniform identifiers
(CURIEs) should look like <code class="language-plaintext highlighter-rouge">NCBI:txid175694</code>, OBO Foundry says the namespace is
<code class="language-plaintext highlighter-rouge">NCBITaxon</code> (one of the few notable mixed-case namespace names) and CURIEs
should look like <code class="language-plaintext highlighter-rouge">NCBITaxon:175694</code>.</p>

<p>Identifiers.org came along to solve some of these ambiguities with a curated
database, but it’s missing lots of the things in OBO Foundry and OLS, and it
even disagrees on others. They call the NCBI taxonomy namespace <code class="language-plaintext highlighter-rouge">taxonomy</code> and
say that identifiers should look like <code class="language-plaintext highlighter-rouge">taxonomy:175694</code>. Exhausting!</p>

<p><img src="/img/registry_comparison.svg" alt="Registry Comparison" /></p>

<p>One more issue is the GOGO problem. Many OBO ontologies use local identifiers
that also include the prefix because a given ontology might contain terms
imported from other ones. However, this means that ontologies that originated
from the OBO world have redundant identifiers, like from GO (e.g.,
GO:GO:0012501). I know what you’re wondering: is Dr. Claw in charge? Maybe.</p>

<hr />

<p>The reason I went down this rabbit hole is because I want to support people to
do better curation. This means I want them to use identifiers instead of ever
changing names. For example, it turns out the half life of an HGNC gene symbol
is very short -
<a href="https://github.com/bio2bel/bio2bel-notebooks/blob/master/gene_symbol_half_life.ipynb">thousands of them change every year</a>.
However, if I want people to use identifiers instead of names in their
databases, their papers, and other writing, there need to be really good tools
for looking up the names that go with each identifier and the cross-references
(equivalences) to other databases that are talking about the same thing.</p>

<p>So I built <a href="https://github.com/pyobo/pyobo">PyOBO</a>. It includes tools for
reading the OBO Foundry and getting all of the OBO resources that are available
(as well as <em>many</em> manual fixes for incorrect metadata), it uses Daniel
Himmelstein’s <a href="https://github.com/dhimmel/obonet/">Obonet</a> for parsing and
storing pre-parsed files for fast loading, and it applies a swath of rule-based
normalization that I’ve
<a href="https://github.com/pyobo/pyobo/blob/master/src/pyobo/registries/metaregistry.json">manually curated</a>
by personally reading all of the OBO files, their identifiers, their
cross-references, relationships, properties, and everything else. When it comes
to data, there really is no way around getting your hands dirty.</p>

<p>I also went ahead and
<a href="https://github.com/pyobo/pyobo/tree/master/src/pyobo/sources">wrote parsers and converters</a>
for lots of other databases like Entrez, ComplexPortal, InterPro, and others so
they could play nice with the rest of the ecosystem. Of course, this is an
ongoing process. There are always more databases to include, and when it comes
to super-sized ones like PubChem, the paradigms I used might not hold up anymore
(though I did write parser/converter for it and you’re welcome to use it).</p>

<p>After this long journey of a blog post, I think we’re ready to address Rihanna’s
perrenial question: what’s my name? Until now, there really didn’t exist a
service that let you look up the name for an entity by its CURIE. The link I
gave for the OLS is the closest I have found, and that just doesn’t cut it.</p>

<p>After all of this coding, I wrote a script (just run <code class="language-plaintext highlighter-rouge">obo ooh-na-na</code>) that takes
all of the available sources, normalizes their namespaces, normalizes their
identifiers, and dumps them as a big ‘ol TSV file. 3 columns - namespace,
identifier, and name. No nonsense. Probably legal! Get it at
<a href="https://doi.org/10.5281/zenodo.3756206"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.3756206.svg" alt="DOI" /></a>.
I’ll make updates periodically as I add more sources, such as if/when I feel
comfortable with including the PubChem dump - the
<a href="ftp://ftp.ncbi.nlm.nih.gov/pubchem/Compound/Monthly/2020-04-01/Extras/CID-Title.gz">CID-Title.gz</a>
file is about 1.3 gigabytes, which means this will significantly increase the
size, but not so much that it’s unreasonable.</p>

<p>I can imagine that most people probably won’t want to download this file, or
load it in memory (un-gzipped) every time they want to use it. I wrote a simple
web service that wraps this dataset
<a href="https://github.com/pyobo/pyobo/blob/master/src/pyobo/apps/resolver.py">included in PyOBO</a>.
It should be as easy as running with the shell with
<code class="language-plaintext highlighter-rouge">python -m pyobo.apps.resolver</code> then running the following python code:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">requests</span>

<span class="c1"># This is an exact match
</span><span class="n">successful_request</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://localhost:5000/resolve/DOID:14330'</span><span class="p">).</span><span class="n">json</span><span class="p">()</span>
<span class="c1"># {"identifier": "14330", "name": "Parkinson's disease", "prefix": "doid", "query": "DOID:14330", "success": True}
</span>
<span class="c1"># This one remaps the prefix if you get it slightly wrong
</span><span class="n">successful_remapped_request</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://localhost:5000/resolve/DO:14330'</span><span class="p">).</span><span class="n">json</span><span class="p">()</span>
<span class="c1"># {"identifier": "14330", "name": "Parkinson's disease", "prefix": "doid", "query": "DO:14330", "success": True}
</span>
<span class="c1"># This one can't find the identifier.
</span><span class="n">unsuccessful_request</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://localhost:5000/resolve/DO:00000'</span><span class="p">).</span><span class="n">json</span><span class="p">()</span>
<span class="c1"># {"identifier": "00000", "message": "Could not look up identifier", "prefix": "doid", "query": "DO:00000", "success": False}
</span>
<span class="c1"># Keep in mind, the point of this service isn't to validate identifiers.
</span><span class="n">unsuccessful_crazy_request</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://localhost:5000/resolve/DO:thisIsNotRightAtAll'</span><span class="p">).</span><span class="n">json</span><span class="p">()</span>
<span class="c1"># {"identifier": "thisIsNotRightAtAll", "message": "Could not look up identifier", "prefix": "doid", "query": "DO:thisIsNotRightAtAll", "success": False}
</span>
<span class="c1"># No mercy for bad prefixes
</span><span class="n">unsuccessful_prefix_lookup</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://localhost:5000/resolve/notanamespace:0000'</span><span class="p">).</span><span class="n">json</span><span class="p">()</span>
<span class="c1"># {"message": "Could not identify prefix", "query": "notanamespace:0000", "success": False}
</span></code></pre></div></div>

<p>It’s especially important that the service normalizes curies first, so both
<code class="language-plaintext highlighter-rouge">DOID:14330</code>, <code class="language-plaintext highlighter-rouge">doid:14330</code>, and <code class="language-plaintext highlighter-rouge">DO:14330</code> can all be resolved to their name,
<em>Parkinson’s disease</em>. Because I did extensive manual curation of namespaces and
their synonyms, <code class="language-plaintext highlighter-rouge">NCBITaxon</code> and <code class="language-plaintext highlighter-rouge">taxonomy</code> are both acceptable as well. However,
this service doesn’t load from the aforementioned TSV, but rather takes
advantage of PyOBO’s internal code for looking up mappings. I can imagine lots
of ways I might re-write this service to directly take advantage of this dump (I
also invite you to do the same, however best suits you) such as loading it into
EdgeDB and auto-generating a GraphQL endpoint.</p>

<p>The last thing that I’m looking into getting this service hosted so everyone can
benefit from it without doing dev-ops in their own organizations. Then I will
continue to obfuscate all usage and documentation with references to pop
culture. Enjoy!</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="OBO" /><category term="lexica" /><summary type="html"><![CDATA[We have a big problem in the bioinformatics community with namespaces, identifiers, and names. And nobody’s posed the question better than Rihanna herself.]]></summary></entry><entry><title type="html">Summarizing ChemRxiv</title><link href="https://cthoyt.com/2020/04/15/summarizing-chemrxiv.html" rel="alternate" type="text/html" title="Summarizing ChemRxiv" /><published>2020-04-15T08:00:00+00:00</published><updated>2020-04-15T08:00:00+00:00</updated><id>https://cthoyt.com/2020/04/15/summarizing-chemrxiv</id><content type="html" xml:base="https://cthoyt.com/2020/04/15/summarizing-chemrxiv.html"><![CDATA[<p>A few months ago, the question was posed on science Twitter: “How many people
have published on <a href="https://chemrxiv.org/">ChemRxiv</a>?”</p>

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="en" dir="ltr">makes me wonder about the stats at <a href="https://twitter.com/ChemRxiv?ref_src=twsrc%5Etfw">@ChemRxiv</a> <a href="https://t.co/Ml5X8F4ckJ">https://t.co/Ml5X8F4ckJ</a></p>&mdash; Egon Willighⓐgen (@egonwillighagen) <a href="https://twitter.com/egonwillighagen/status/1219193083792969728?ref_src=twsrc%5Etfw">January 20, 2020</a></blockquote>

<p>It was a good day for me, which meant I was in the mood to take up the first
challenged posed on Twitter. I found that François-Xavier Coudert
(<a href="https://github.com/fxcoudert">@fxcoudert</a>) has previously written a
<a href="https://github.com/fxcoudert/tools/blob/master/chemRxiv/chemRxiv.py">python client</a>
for ChemRxiv. I made a pair of pull requests
(<a href="https://github.com/fxcoudert/tools/pull/9">fxcoudert/tools#9</a> and
<a href="https://github.com/fxcoudert/tools/pull/10">fxcoudert/tools#10</a>) to fix some
bugs and make it importable from other python modules.</p>

<p>Unlike BioRxiv, the pre-print server for biology, ChemRxiv is implemented with
<a href="https://figshare.com/">FigShare</a>. It turns out that all FigShare “institutions”
like ChemRxiv are actually accessible through the main
<a href="https://docs.figshare.com/">FigShare API</a>. I think this is pretty cool, and
made sure that the ChemRxiv client that I had updated was actually able to be
run for any institution. Fun fact: the institution code for ChemRxiv is <code class="language-plaintext highlighter-rouge">259</code>.</p>

<p>I got to work writing my
<a href="https://github.com/cthoyt/chemrxiv-summarize">own repository</a> to wrap the
client, take care of downloading all of the bibliographic information available,
and generating some pretty pictures. I originally ran the scripts and generated
pictures on January 20th, 2020 (the day Egon posed the question). Since the
pandemic has got the whole science community introspecting, I came back to this
today and thought it might be worth writing up as a blog post.</p>

<p>Without further ado, here are the most recent charts I’ve generated to answer
three main questions. I’ve linked the images in such a way that the charts will
be automatically updated with my GitHub repository. This also implicitly means
that there’s a history of each image, but because two of them are plotting time
course information, the history is already conveyed within the chart.</p>

<h3 id="how-many-articles-were-contributed-each-month-to-chemrxiv">How many articles were contributed each month to ChemRxiv?</h3>

<p>How many papers were submitted each month to ChemRxiv? Keep in mind that the
current month is likely not complete.</p>

<p><img src="https://raw.githubusercontent.com/cthoyt/chemrxiv-summarize/master/figshare/chemrxiv/articles_per_month.png" alt="Articles per Month" /></p>

<h3 id="how-many-unique-authors-contribute-each-month-to-chemrxiv">How many unique authors contribute each month to ChemRxiv?</h3>

<p>This only counts using the ORCID iDs of the first authors; it’s pretty
inconsistent what other identifying information is included in the metadata for
each article.</p>

<p><img src="https://raw.githubusercontent.com/cthoyt/chemrxiv-summarize/master/figshare/chemrxiv/unique_authors_per_month.png" alt="Unique Authors per Month" /></p>

<h3 id="how-many-author-submit-multiple-times-each-month">How many author submit multiple times each month?</h3>

<p>How many authors submitted more than once per month? This chart shows spikes in
August, which I will guess is when most people are submitting before their
summer breaks :)</p>

<p><img src="https://raw.githubusercontent.com/cthoyt/chemrxiv-summarize/master/figshare/chemrxiv/percent_duplicate_authors_per_month.png" alt="Percent Duplicate Authors per Month" /></p>

<h3 id="how-many-authors-submitted-for-their-first-time-each-month">How many authors submitted for their first time each month?</h3>

<p><img src="https://raw.githubusercontent.com/cthoyt/chemrxiv-summarize/master/figshare/chemrxiv/first_time_first_authors_per_month.png" alt="First Time First Authors per Month" /></p>

<h3 id="how-many-unique-first-authors-are-there-on-chemrxiv">How many unique first authors are there on ChemRxiv?</h3>

<p>How many first authors have historically contributed to ChemRxiv at each month?
We can take the first date of authorship for each author then count at each
month how many unique first time authors there are. Then, we can use a
cumulative sum to show how many authors have contributed to ChemRxiv at any
point in time.</p>

<p><img src="https://raw.githubusercontent.com/cthoyt/chemrxiv-summarize/master/figshare/chemrxiv/historical_authorship.png" alt="Historical Authorship" /></p>

<h3 id="how-many-authors-are-prolific-on-chemrxiv">How many authors are prolific on ChemRxiv?</h3>

<p>If we aggregate the data, we can ask how many authors have submitted lots of
articles:</p>

<p><img src="https://raw.githubusercontent.com/cthoyt/chemrxiv-summarize/master/figshare/chemrxiv/author_prolificness.png" alt="Author Prolificness" /></p>

<h3 id="what-licenses-are-popular-on-chemrxiv">What licenses are popular on ChemRxiv?</h3>

<p>The following chart shows the popularity of different licenses over time. The
<a href="https://creativecommons.org/licenses/by-nc-nd/4.0/">CC BY-NC-ND 4.0 license</a> is
a resounding victor. You can learn about Creative Commons (CC) licenses
<a href="https://creativecommons.org/licenses/">here</a>.</p>

<p><img src="https://raw.githubusercontent.com/cthoyt/chemrxiv-summarize/master/figshare/chemrxiv/historical_licenses.png" alt="Historical Licenses" /></p>

<p>If you’re interested to regenerate these charts yourself, you’re welcome to do
so with the following code:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/cthoyt/chemrxiv-summarize
<span class="nb">cd </span>chemrxiv-summarize
python 01_download.py
python 02_process.py
python 03_visualize.py
</code></pre></div></div>

<p>Downloading takes a bit of time (about 40 minutes) but there’s a <code class="language-plaintext highlighter-rouge">tqdm</code> bar to
keep you entertained in the mean time. Normally I package all of my code, but
the one off scripts here didn’t seem to warrant it.</p>

<p>As a final note, I’d like to shout out to Marshall Brennan
(<a href="https://twitter.com/Organometallica">@Organometallica</a>) for being an excellent
spokesperson and public face of ChemRxiv. Also, throughout this process I
realized he also was a chemistry major in his bachelor’s at Northeastern
University like me. Go huskies!</p>

<hr />

<p>May 2020 Update: François-Xavier Coudert created the
<a href="https://chemrxiv-dashboard.github.io/">ChemRxiv-Dashboard</a>, which makes some
similar summaries to this. Check it out!</p>

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="en" dir="ltr">I made a dashboard for <a href="https://twitter.com/ChemRxiv?ref_src=twsrc%5Etfw">@ChemRxiv</a>, fed by the <a href="https://twitter.com/figshare?ref_src=twsrc%5Etfw">@figshare</a><br />metadata API.<a href="https://t.co/rKyAOGkrVO">https://t.co/rKyAOGkrVO</a> <a href="https://t.co/fLfjEabraz">pic.twitter.com/fLfjEabraz</a></p>&mdash; FX Coudert (@fxcoudert) <a href="https://twitter.com/fxcoudert/status/1262763710956793860?ref_src=twsrc%5Etfw">May 19, 2020</a></blockquote>

<p>November 2020 Update: I added a license chart and made some changes to enable
this repo to be much more easily used for other FigShare institutions. If you’ve
found this post from @figshare’s
<a href="https://twitter.com/figshare/status/1323762002293121025">tweet</a> and want help
making these charts for your FigShare institution, please feel free to @ me on
Twitter or send me an email.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="bibliometrics" /><summary type="html"><![CDATA[A few months ago, the question was posed on science Twitter: “How many people have published on ChemRxiv?”]]></summary></entry><entry><title type="html">How to Fix Your Monolithic Pull Request</title><link href="https://cthoyt.com/2020/03/20/how-to-fix-your-monolithic-pull-request.html" rel="alternate" type="text/html" title="How to Fix Your Monolithic Pull Request" /><published>2020-03-20T08:00:00+00:00</published><updated>2020-03-20T08:00:00+00:00</updated><id>https://cthoyt.com/2020/03/20/how-to-fix-your-monolithic-pull-request</id><content type="html" xml:base="https://cthoyt.com/2020/03/20/how-to-fix-your-monolithic-pull-request.html"><![CDATA[<p>We’ve all been there. You started a new branch from master. You had a very
specific goal in mind, <strong>The Original Goal</strong>. You made a pull request (PR) to go
with it, too, <strong>The Original Pull Request</strong>. But then, you had an idea! And
also, someone on your team asked you to solve another problem! Now the original
code you wrote to address <strong>The Original Goal</strong> relies on that code … and now
you’ve got dozens of files changed, hundreds of lines of diff, and nobody
(including you) can understand what you’ve done. Like I said, we’ve all been
there. Here’s what you can do to fix it:</p>

<h2 id="1-stop-and-relax">1. Stop and Relax</h2>

<p>Don’t do anything rash. Git is a pain to use, and you’re going to have to rely
on it to keep a history for you of what you’ve done.</p>

<h2 id="2-summarize">2. Summarize</h2>

<p>First, you’re going to have to take a big step back. Write a summary of all the
things you’ve done in <strong>The Original Pull Request</strong>. This should be about <em>what</em>
the PR does and <em>why</em> it does it. Of course it could vary depending on the
situation, but this summary shouldn’t be about exactly how the PR does it,
because the implementation details are likely what lead to this situation in the
first place.</p>

<p>Keep in mind that every PR has a box at the top that’s used to describe what’s
in it. This is where you will put your summary.</p>

<h2 id="3-assessing-dependencies">3. Assessing Dependencies</h2>

<p>Of all the things that <strong>The Original Pull Request</strong>, some of them are
self-contained, and some of them rely on each other. It was probably the case
that to accomplish <strong>The Original Goal</strong>, you had to address lots of smaller
goals. You probably also had to change lots of code and write new code too.</p>

<p>Wouldn’t it have been nice if all of these implementations were already done,
because then you could have just solved <strong>The Original Goal</strong> directly by
using/applying previous code. That’s what we’re going to aim for.</p>

<p>But first, you need to figure out which things you did relied on which other
ones, because you’re going to break <strong>The Original Pull Request</strong> up until it
exactly matches up to addressing <strong>The Original Goal</strong>. don’t have any</p>

<h2 id="4-the-break-up">4. The Break Up</h2>

<p>After you understand which parts of <strong>The Original Pull Request</strong> depend on each
other, pick one independent part of the code that accomplishes one sub-goal.
Since you’re not doing this to be a martyr, and we all know git is too
complicated to <em>Do It Right</em>, you’re going to copy/paste the files that are
related to this change to your desktop*.</p>

<h2 id="5-escape-the-madness">5. Escape the Madness</h2>

<p>Before continuing, you’re going to make sure all of the code in your big messy
branch for <strong>The Original Pull Request</strong> is committed and pushed. Even though we
want to supersede what’s there, it never hurts to keep track of your descent
into madness.</p>

<p>After there’s nothing lying around, switch back to master. If your team has
taken good care of your repository, the master branch should be undisturbed by
the chaos you’ve created in <strong>The Original Pull Request</strong>. Make a new branch
from master, and name it appropriately for fixing the one sub-goal, from here
out known as <strong>The Sub-Goal</strong> that you identified in Step 4. Now you can start
updating the relevant files in your repository based on the files you copied to
your desktop. I suggest you don’t copy/paste the contents of the whole files,
because you might have forgotten about something else you changed in them. After
all, you’re reading my guide because this was a mess.</p>

<h2 id="6-the-new-pull-request">6. The New Pull Request</h2>

<p>Once you’ve finished making the new branch for your independent part of code
that solves <strong>The Sub-Goal</strong>, you can make <strong>The New Pull Request</strong>.</p>

<p>You will now go through the entire process of writing a good summary of this
branch for your co-developers, you will get their feedback, you will make
updates, pass flake8, and so on. They will thank you for having code that
accomplishes one thing, and can be described simply. They will thank you for not
having too big of a diff, and for the things in the diff all being relevant and
important. Then you can merge this branch into master.</p>

<h2 id="7-newfound-wisdom">7. Newfound Wisdom</h2>

<p>Throughout transferring the code for <strong>The New Pull Request</strong> you have probably
realized there are some things you did back in <strong>The Original Pull Request</strong>
that you could do better, and made some updates in the code in <strong>The New Pull
Request</strong> to reflect the wisdom you’ve gained along the way. That’s great!
Congratulations!</p>

<p>After your team has approved <strong>The New Pull Request</strong>, you can merge it into
master and both delete the branch locally and on the remote. Then you should
switch back to the master branch. You can pull from master, and see your code
that solved <strong>The Sub-Goal</strong> reflected here.</p>

<h2 id="8-the-hard-part">8. The Hard Part</h2>

<p>This is the hard part. Now you have to switch back to the branch for <strong>The
Original Pull Request</strong>. Now you have to update this branch from master. It’s
going to be hard because now you’ve probably made different changes in <strong>The New
Pull Request</strong> than in <strong>The Original Pull Request</strong> so there will likely be
conflicts.</p>

<p>This is not a tutorial on how to solve merge conflicts. Use google to figure
that out</p>

<p>I can’t understate: <strong>do this part really well</strong>. If you don’t, then the history
in the original branch will be even more incomprehensible, and you won’t be able
to tell if you lost any of your original work. Please, please, please do this
well.</p>

<p>P.S. Like I said before, don’t be a martyr. Use tools like GitHub Desktop and
PyCharm to help you merge. I heard that the git CLI was <em>allegedly</em> created by
Linus Torvalds to slow other developers down.</p>

<p>Why are we going through all of this pain, rather than just pushing your team to
let you merge <strong>The Original Pull Request</strong>? The reason you have to do this is
because now all of the changes that addressed <strong>The Sub-Goal</strong> are part of
master, and are no longer part of the diff of <strong>The Original Pull Request</strong>.</p>

<p>Now you’re one step closer to your team being able to understand, review, and
eventually merge <strong>The Original Pull Request</strong>.</p>

<h2 id="9-the-frustrating-part">9. The Frustrating Part</h2>

<p>This is the frustrating part. After you’ve gone through all of that work to
split a tiny part of <strong>The Original Pull Request</strong> into a smaller, independent
pull request, you’re not done. You will probably have to repeat steps 4-8 a few
times. You’ll be tempted to throw away the branch for <strong>The Original Pull
Request</strong> and maybe start over.</p>

<p>Don’t do that.</p>

<p>If you do, the same disorganization that lead to the mess of <strong>The Original Pull
Request</strong> might just slip back into whatever you do next. Even worse, nobody
else will be able to follow what you’ve done until now.</p>

<p>So relax. This is going to take a few days. You’re going to have to wait in
between several iterations for feedback. That’s good. You need feedback. I need
feedback. We all need to practice getting it and giving it. Embrace the
opportunity to have your team help you improve your code, gain wisdom, and make
your contributions sustainable.</p>

<h2 id="finishing-up">Finishing Up</h2>

<p>Eventually after several iterations of 4-9, you will have excised all of the
code that was important for <strong>The Original Pull Request</strong>, but not directly
accomplishing <strong>The Original Goal</strong>. As you removed independent parts, new parts
became independent themselves. Eventually, <strong>The Original Pull Request</strong> will
indeed match up exactly to <strong>The Original Goal</strong>, then you will be able to come
back to it for review and merging.</p>

<p>I understand this is a frustrating process. The purpose of these steps were to
help you think through a large piece of work you’ve done. You should be proud
that you’ve solved a complex problem with many intricate parts. It was a lot of
extra work to break it into many pull requests, and it might have taken more of
your time the first time working through this process, but in the future, this
might help you to start with small tasks rather than addressing <strong>The Original
Goal</strong> all at once. GitHub, for example, has an issue tracker that is very
helpful for this. I imagine that each issue should correspond to a <strong>Sub-Goal</strong>,
and that each should have exactly one PR that addresses it. <strong>The Original
Goal</strong> also deserves its own issue that points to all of the issues for its
sub-goals. Eventually you will address this with a beautiful PR as well. Happy
coding!</p>

<p>*If you’re thinking, why don’t I use cherry picking? If you know what cherry
picking is in the context of git (and also how to use it) then you probably
won’t have the issue that prompted this blog post. But also, you should go
outside and pick some apples instead. Thanksgiving is never more than a few
hundred days away. It pays to be ready.</p>

<h2 id="afterword">Afterword</h2>

<p>It might be illustrative to see where an example of where this was done in
practice, so I’ll share some work I did with a text mining tool from Harvard
Medical School, <a href="https://github.com/indralab/gilda">Gilda</a>. It’s a simple yet
powerful system for grounding of named entities based on dictionary lookup.
Unfortunately, it didn’t include some dictionaries I wanted, and it didn’t have
a UI to go with its web API.</p>

<p>So I set out on figuring out how it generated dictionaries, where it stored
them, and how it loaded them to make the web app. I ended up making several
modifications to accomplish this goal, but it was a huge PR. I’ve definitely
annoyed the author, <a href="https://github.com/bgyori">@bgyori</a>, with PRs that are too
big before, which he was ultimately not able to understand or merge.</p>

<p>Keep in mind, in your team, your teammates might be obligated to help you
because you’re working towards a common goal, getting paid, etc. When you’re in
the open source world, nobody really owes you anything, so you it’s in your best
interest to make things as easy as possible on the package’s maintainer(s).</p>

<p>So I made a few different pull requests that were all totally independent:</p>

<ul>
  <li>Add constants for resource file paths
<a href="https://github.com/indralab/gilda/pull/12">#12</a></li>
  <li>Make API more reusable <a href="https://github.com/indralab/gilda/pull/13">#13</a></li>
  <li>Make instantiation of Grounder more flexible
<a href="https://github.com/indralab/gilda/pull/15">#15</a></li>
</ul>

<p>Maybe you’re seeing a theme here. I was improving lots of different bits of
Gilda so I could reuse the package in new code later. The next incremental
increase was:</p>

<ul>
  <li>Refactor functionality from the GrounderInstance class into the Grounder class
<a href="https://github.com/indralab/gilda/pull/16">#16</a></li>
</ul>

<p>And finally with these in place, I realized that adding a web interface was
parallel to my original goal, but not the core. What was really important was
that throughout all of the Gilda functionality, I could load my own synonym list
(which I’d generate using the HPO, EFO, and DOID). I was able to address the UI
with:</p>

<ul>
  <li>Add minimimal UI to web interface
<a href="https://github.com/indralab/gilda/pull/19">#19</a></li>
</ul>

<p>At the time of writing, we’re still working through this PR. But all of it is
leading up to the point where I can load my own files into this web interface.
It will seem so obvious to Ben when I send this PR next (but after giving him
some space… I did just bombard him with 5 PRs in a few days) what I am trying
to accomplish and why.</p>

<p>Want to see what happens when you try and do all of this in one PR? You will
correctly guess that the PR is a total mess, impossible to understand, and
riddled with questions that are really too big to answer when your head is
already so far in the sand. Behold, in all its infamy, my failed PR from last
summer (<a href="https://github.com/indralab/gilda/pull/4">#4</a>). At this point, you
can’t even see what a mess it was from the linked web page but if you go back
through the version history before I broke it into 5 smaller PRs (using the
workflow described above) it was a monolith.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="code with me" /><summary type="html"><![CDATA[We’ve all been there. You started a new branch from master. You had a very specific goal in mind, The Original Goal. You made a pull request (PR) to go with it, too, The Original Pull Request. But then, you had an idea! And also, someone on your team asked you to solve another problem! Now the original code you wrote to address The Original Goal relies on that code … and now you’ve got dozens of files changed, hundreds of lines of diff, and nobody (including you) can understand what you’ve done. Like I said, we’ve all been there. Here’s what you can do to fix it:]]></summary></entry><entry><title type="html">Host a Graduate Seminar Before Writing Your Thesis</title><link href="https://cthoyt.com/2020/02/09/seminar-for-thesis-writing.html" rel="alternate" type="text/html" title="Host a Graduate Seminar Before Writing Your Thesis" /><published>2020-02-09T08:00:00+00:00</published><updated>2020-02-09T08:00:00+00:00</updated><id>https://cthoyt.com/2020/02/09/seminar-for-thesis-writing</id><content type="html" xml:base="https://cthoyt.com/2020/02/09/seminar-for-thesis-writing.html"><![CDATA[<p>The other day I saw a tweet lamenting the drag that is literature review during
preparation for writing your thesis.</p>

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="en" dir="ltr">I just love writing 15 page literature reviews for graduate school courses on literally any topic except my thesis topic.</p>&mdash; PhD Diaries (@thoughtsofaphd) <a href="https://twitter.com/thoughtsofaphd/status/1225762592045649920?ref_src=twsrc%5Etfw">February 7, 2020</a></blockquote>

<p>I agree. I felt the same pain last fall when I wrote
<a href="https://github.com/cthoyt/doctoral-thesis">my doctoral thesis</a>. Luckily, I had
a strategy that made it a bit easier.</p>

<p>I learned it from one of my professors when I was doing my master’s degree in
Life Science Informatics. Each semester, we had a seminar course in which each
student was assigned research articles to read and present to the class with a
short slide deck. Later, I joined his research group and realized that this
course served as a literature review for him just as much as us.</p>

<p>So later when I was a Ph.D. student, I volunteered to run the seminar. I
co-opted the concept, and planned the course to cover many of the topics I found
interesting for my thesis. I already knew some of the papers very well, and a
few were ones I had always been meaning to read. I tried to pick the most recent
papers for topics when possible, but also threw in a few classics as well.</p>

<p>On the first day of the seminar, I shared the following course information. I
thought it was important to make clear what my expectations were for students in
terms of their prior knowledge. Since they all came from the same master’s
program, I thought it was enough that they had passed one of the first semester
lectures called “Biological Databases” which was about many of the resources and
databases used in the systems and networks biology community. I also outlined
what was the content for the course, what was expected, etc. then shared this
all as a Google Doc so they could read it over and add comments.</p>

<p>I also made a list of possible papers and a tentative schedule that students
could look over and decide which papers they found most interesting. The topics
were arranged in a logical order to tell the story of my thesis, and for each
section there were a few papers that I thought were very important, and a few
extras just in case there was a lot of interest. During the first day of the
seminar, I also went through the list of all papers and explained the topics to
the students. I gave them this list via Google Docs as well, and they were able
to claim papers for their presentations. Below, I’ve listed the final list of
papers and the order in which they were presented. We were able to come to
agreements for all students to present the papers I found most important. Maybe
40% of the class found a paper interesting and picked one the first day and the
rest took the next week to decide, ask questions, or propose new papers.</p>

<p>Another consideration I had when picking this paper list was to choose work done
by my colleagues that I found interesting and helpful. After, I invited them to
come listen to the seminar and mediate discussion after. We were able to invite
one of my collaborators Mehdi Ali (he’s a really good guy!) to discuss his work
on using deep learning for relation extraction in natural language processing. I
think that might have been the most engaging day of the whole series.</p>

<p>I added one aspect to this course compared to the previous seminar that I had
attended: each student was not only responsible for presenting the paper that
had been assigned from my list, but they were also responsible for finding a
relevant pre-print (in the same or similar topic) and submitting a peer review
through the pre-print system. When I was a student, I noticed many students did
not read the references of the paper they were assigned in our seminars, and
also had not considered other similar research to their paper. Asking them to
find their own papers was a way to make this a more creative and fun process,
and would directly prepare them to answer questions at the end of the
presentation like “what will the authors do next?” or “how will this research be
used by others?”</p>

<p>One of the funny things that happened during the pre-print presentations is the
students found several of mine and presented those. I suppose this was
inevitable given the contemporary nature of my work in the context of the topics
chosen. I would actually explicitly encourage students to check out my
pre-prints the next time I host a seminar, because I know the work very well and
could mediate a nice discussion.</p>

<p>I learned a lot through the process of preparing this seminar. Its outline
became the outline for my thesis, and a lot of the discussions became points
that I addressed explicitly in my writing. I wouldn’t say that I was taking
advantage of the students in this process - we all benefited from the
experience. I hope you get some ideas about how you might be able to do this
yourself, whether you’re a doctoral student, a postdoc, or something else!</p>

<h2 id="course-information">Course Information</h2>

<ul>
  <li>Title: Knowledge Assembly, Data Integration, and Modeling in Systems and
Networks Biology</li>
  <li>Period: Winter Semester 2018/2019</li>
  <li>Location: Endenicher Allee 19A, Room U.105 on Wednesdays 13.00-14.30</li>
</ul>

<h3 id="qualifications">Qualifications</h3>

<p>Students should be comfortable with the material presented in the Biological
Databases lecture during the first semester of the LSI curriculum.</p>

<h3 id="goal">Goal</h3>

<p>Students will have the opportunity to practice reading, presenting, and
discussing recent biomedical literature on the topics of knowledge assembly,
data integration, and modeling in systems and networks biology.</p>

<h3 id="content">Content</h3>

<p>Students will be assigned papers and present on the holistic process of
knowledge discovery in systems and networks biology that focus on the topics of
knowledge assembly (e.g., natural language processing, modeling formalisms and
formats, reasoning techniques), data integration (e.g., practical scenarios
focusing on techniques on the data level, knowledge level, and analytical
levels), and modeling strategies (e.g., rule-based modeling, agent-based
modeling, mathematical modeling, hypothesis generation with knowledge-based
approaches).</p>

<h3 id="assignment">Assignment</h3>

<p>Students will be assigned an article to read and present during a thirty (30)
minute lecture. One goal of this lecture is to show an understanding of not only
the material presented in the article, but also the relevant background
information - this may entail following the references and reading other
articles. Another goal is to not only educate, but entertain the audience.
Students will also be expected to find a relevant pre-print article on arXiv,
bioRxiv, or other pre-print server and post a peer-review for the author on the
corresponding service. Following the presentation of their assigned article,
students should include slides (1-3) briefly explaining the relevance of the
pre-print that they found.</p>

<h2 id="method-of-performance-review">Method of Performance Review</h2>

<p>Students will be assessed on the understanding of their assigned topic, the
quality of their presentation, and their participation. Students missing more
than 2 seminars will not pass the course without a doctor’s note.</p>

<h2 id="schedule">Schedule</h2>

<h3 id="week-0---october-10th-2018---syllabus-week">Week 0 - October 10th, 2018 - Syllabus Week</h3>

<p>This week there will a short discussion of the syllabus and no presentation. For
those in Bonn that aren’t aware of this wonderful tradition, welcome to Syllabus
Week.</p>

<h3 id="week-1---october-31st-2018---named-entity-recognition">Week 1 - October 31st, 2018 - Named Entity Recognition</h3>

<p>Mubassher Leser, U., &amp; Hakenberg, J. (2005).
<a href="https://doi.org/10.1093/bib/6.4.357">What makes a gene name? Named entity recognition in the biomedical literature</a>.
Briefings in Bioinformatics, 6(4), 357–369.</p>

<p>Pre-print: https://www.biorxiv.org/content/early/2017/03/08/115022</p>

<p>Bachman, J. A., Gyori, B. M., &amp; Sorger, P. K. (2018).
<a href="https://doi.org/10.1186/s12859-018-2211-5">FamPlex: A resource for entity recognition and relationship resolution of human protein families and complexes in biomedical text mining</a>.
<em>BMC Bioinformatics</em>, 19(1), 1–14.</p>

<p>Pre-print: https://www.biorxiv.org/content/early/2018/07/29/379446</p>

<h3 id="week-2---november-7th-2018---identifiers">Week 2 - November 7th, 2018 - Identifiers</h3>

<p>Laibe, C., &amp; Le Novère, N. (2007).
<a href="https://doi.org/10.1186/1752-0509-1-58">MIRIAM Resources: tools to generate and resolve robust cross-references in Systems’ Biology</a>.
<em>BMC Systems Biology</em>, 1, 58.</p>

<p>Pre-print: https://www.biorxiv.org/content/early/2017/07/24/167619</p>

<p>Juty, N., Le Nover̀e, N., &amp; Laibe, C. (2012).
<a href="https://doi.org/10.1093/nar/gkr1097">Identifiers.org and MIRIAM Registry: Community resources to provide persistent identification</a>.
<em>Nucleic Acids Research</em>, 40(D1), 580–586.</p>

<p>Pre-print: https://www.biorxiv.org/content/early/2018/02/14/101279</p>

<h3 id="week-3---november-14th-2018---information-extraction">Week 3 - November 14th, 2018 - Information Extraction</h3>

<p>Novichkova, S., <em>et al.</em> (2003).
<a href="https://doi.org/10.1093/bioinformatics/btg207">MedScan, a natural language processing engine for MEDLINE abstracts</a>.
<em>Bioinformatics</em>, 19(13), 1699–1706.</p>

<p>Pre-print: https://www.biorxiv.org/content/early/2018/08/29/403667</p>

<p>Ali, M., <em>et al.</em> (2017).
<a href="http://publica.fraunhofer.de/eprints/urn_nbn_de_0011-n-4972978.pdf">Automatic Extraction of BEL-Statements based on Neural Networks</a>.
<em>Proceedings of BioCreative VI Challenge and Workshop</em>, (October).</p>

<p>Pre-print : https://osf.io/j76y3/</p>

<h3 id="week-4---november-21nd-2018---knowledge-representations">Week 4 - November 21nd, 2018 - Knowledge Representations</h3>

<p>Demir, E., <em>et al.</em> (2010).
<a href="https://doi.org/10.1038/nbt1210-1308c">The BioPAX community standard for pathway data sharing</a>.
<em>Nature Biotechnology</em>, 28(12), 1308–1308.</p>

<p>Pre-print: https://www.biorxiv.org/content/10.1101/192856v1</p>

<p>Hucka, M., <em>et al.</em> (2003).
<a href="http://www.ncbi.nlm.nih.gov/pubmed/12611808">The systems biology markup language (SBML): a medium for representation and exchange of biochemical network models</a>.
<em>Bioinformatics (Oxford, England)</em>, 19(4), 524–31.</p>

<p>Pre-print: https://www.biorxiv.org/content/early/2018/01/23/246470</p>

<h3 id="week-5---november-28th---knowledge-representations-cont">Week 5 - November 28th - Knowledge Representations (cont…)</h3>

<p>Le Novère, <em>et al.</em> (2009).
<a href="https://doi.org/10.1038/nbt.1558">The Systems Biology Graphical Notation</a>.
<em>Nature Biotechnology</em>, 27(8), 735–41.</p>

<p>Pre-print: https://www.biorxiv.org/content/early/2018/01/30/256750</p>

<p>Carbon, S., <em>et al.</em> (2017).
<a href="https://doi.org/10.1093/nar/gkw1108">Expansion of the gene ontology knowledgebase and resources: The gene ontology consortium</a>.
<em>Nucleic Acids Research</em>, 45(D1), D331–D338.</p>

<p>Pre-print: https://www.biorxiv.org/content/early/2018/10/07/437020</p>

<h3 id="week-6---december-12th-2018---pathway-databases-and-semantic-data-integration">Week 6 - December 12th, 2018 - Pathway Databases and Semantic Data Integration</h3>

<p>Croft, D., <em>et al.</em> (2014).
<a href="https://doi.org/10.1093/nar/gkt1102">The Reactome pathway knowledgebase</a>.
<em>Nucleic Acids Research</em>, 42(D1), D472–D477. <strong>AND</strong> Fabregat, A., <em>et al.</em>
(2018).
<a href="https://doi.org/10.1093/nar/gkx1132">The Reactome Pathway Knowledgebase</a>.
<em>Nucleic Acids Research</em>, 46(D1), D649–D655.</p>

<p>Pre-print: https://www.biorxiv.org/content/early/2018/10/09/375097</p>

<p>Cerami, E. G., <em>et al.</em> (2011).
<a href="https://doi.org/10.1093/nar/gkq1039">Pathway Commons, a web resource for biological pathway data</a>.
<em>Nucleic Acids Research</em>, 39(SUPPL. 1), 685–690.</p>

<p>Pre-print: https://www.biorxiv.org/content/early/2018/09/03/353235</p>

<p>Khatri, P., Sirota, M., &amp; Butte, A. J. (2012).
<a href="https://doi.org/10.1371/journal.pcbi.1002375">Ten years of pathway analysis: Current approaches and outstanding challenges</a>.
<em>PLoS Computational Biology</em>, 8(2).</p>

<p>Pre-print: https://www.biorxiv.org/content/early/2018/09/13/416131</p>

<p>Gligorijević, V., &amp; Pržulj, N. (2015).
<a href="https://doi.org/10.1098/rsif.2015.0571">Methods for biological data integration: perspectives and challenges</a>.
<em>Journal of The Royal Society Interface</em>, 12(112), 20150571.</p>

<p>Pre-print: https://www.biorxiv.org/content/early/2018/12/13/358390</p>

<h3 id="week-8---january-16th-2019---applications">Week 8 - January 16th, 2019 - Applications</h3>

<p>Saqi, M., <em>et al.</em> (2018).
<a href="https://doi.org/10.1093/bib/bby025">Navigating the disease landscape: knowledge representations for contextualizing molecular signatures</a>.
<em>Briefings In Bioinformatics</em>, (May), 1–15.</p>

<p>Pre-print: https://www.biorxiv.org/content/early/2018/11/23/475202</p>

<p>Himmelstein, D. S., <em>et al.</em> (2017).
<a href="https://doi.org/10.7554/eLife.26726">Systematic integration of biomedical knowledge prioritizes drugs for repurposing</a>.
<em>ELife</em>, 6.</p>

<p>Pre-print: https://www.biorxiv.org/content/early/2018/10/13/442640</p>

<h3 id="week-9---january-23rd-2019---applications">Week 9 - January 23rd, 2019 - Applications</h3>

<p>Lopez, C. F., <em>et al.</em> (2013).
<a href="https://doi.org/10.1038/msb.2013.1">Programming biological models in Python using PySB</a>.
<em>Molecular Systems Biology</em>, 9(646), 646.</p>

<p>Pre-print: https://www.biorxiv.org/content/early/2018/12/23/503359</p>

<p>Gyori, B. M., <em>et al.</em> (2017).
<a href="https://doi.org/10.15252/msb.20177651">From word models to executable models of signaling networks using automated assembly</a>.
<em>Molecular Systems Biology, 13(11)</em>, 954.</p>

<p>Pre-print: https://www.biorxiv.org/content/early/2018/05/15/322156</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="doctoral thesis" /><category term="teaching" /><summary type="html"><![CDATA[The other day I saw a tweet lamenting the drag that is literature review during preparation for writing your thesis.]]></summary></entry><entry><title type="html">Encoding Biology in Knowledge Graphs</title><link href="https://cthoyt.com/2020/01/23/encoding-biology-in-kgs.html" rel="alternate" type="text/html" title="Encoding Biology in Knowledge Graphs" /><published>2020-01-23T08:00:00+00:00</published><updated>2020-01-23T08:00:00+00:00</updated><id>https://cthoyt.com/2020/01/23/encoding-biology-in-kgs</id><content type="html" xml:base="https://cthoyt.com/2020/01/23/encoding-biology-in-kgs.html"><![CDATA[<p>How many molecular biology papers have you read today? This week? This month? If
you’re like me, its not so many, and we’re falling behind very quickly. Here’s a
chart made by the <em>new</em> PubMed that summarizes how many papers were published
mentioning RAS in the last 50 years.</p>

<p><img src="/img/ras_pubmed_history.png" alt="RAS Histogram" /></p>

<p>There were 4,483 publications listed in 2019. We can’t read that much, and even
if we did, we couldn’t remember it all. That’s why we need to take the knowledge
out of the unstructured text and store it in a structured form that can be read
and stored in computers. This way, we can easily share it, query it, and write
algorithms that can help us reason about the incredible amount of biological
knowledge out there.</p>

<p>There are several formats in which this kind of information can be stored on a
continuum between directly representing mechanistic biology to representing the
knowledge itself. In the popular middle ground are BioPAX and BEL, which I’ll
come back to in future posts.</p>

<p>It’s important to keep in mind that knowledge needs to be curated - this can
either be manual, through natural language processing, or a mixture of both.
I’ve written
<a href="https://academic.oup.com/database/article/doi/10.1093/database/baz068/5521414">a paper</a>
on such a process, but for now this post should motivate a few following ones
describing what it takes to deal with nomenclature, build ontologies, and then
start extracting mechanistic biology from the literature.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="knowledge graphs" /><summary type="html"><![CDATA[How many molecular biology papers have you read today? This week? This month? If you’re like me, its not so many, and we’re falling behind very quickly. Here’s a chart made by the new PubMed that summarizes how many papers were published mentioning RAS in the last 50 years.]]></summary></entry><entry><title type="html">Biosemantics vs. Biopragmatics</title><link href="https://cthoyt.com/2020/01/22/biosemantics-versus-biopragmatics.html" rel="alternate" type="text/html" title="Biosemantics vs. Biopragmatics" /><published>2020-01-22T08:00:00+00:00</published><updated>2020-01-22T08:00:00+00:00</updated><id>https://cthoyt.com/2020/01/22/biosemantics-versus-biopragmatics</id><content type="html" xml:base="https://cthoyt.com/2020/01/22/biosemantics-versus-biopragmatics.html"><![CDATA[<p>In language, semantics describe the names and meanings of words. The
bioinformatics community has aptly adopted <em>biosemantics</em> as a concept that
encompasses the issues with the names and meanings of biological entities,
usually in natural language processing and data integration. However, semantics
does not capture the context of words, and <em>biosemantics</em> fails to describe the
biological context and complex relationships between biological entities.</p>

<p><img src="https://pediaa.com/wp-content/uploads/2018/08/Difference-Between-Semantics-and-Pragmatics_Figure-1.png" alt="Semantics versus Pragmatics" height="300px" /></p>

<p>Pragmatics goes beyond semantics and describes the context of words. Because of
this parallelism, I’ve begun to use the term <em>biopragmatics</em> to describe the
family of computational approaches aimed at identifying and contextualizing the
context of biological entities.</p>]]></content><author><name>Charles Tapley Hoyt</name><email>cthoyt@gmail.com</email><uri>https://orcid.org/0000-0003-4423-4370</uri></author><category term="semantics" /><category term="meta" /><summary type="html"><![CDATA[In language, semantics describe the names and meanings of words. The bioinformatics community has aptly adopted biosemantics as a concept that encompasses the issues with the names and meanings of biological entities, usually in natural language processing and data integration. However, semantics does not capture the context of words, and biosemantics fails to describe the biological context and complex relationships between biological entities.]]></summary></entry></feed>