Bioinformatics Databases

Lesson 17 of 20 · 12 min

ID Mapping in Practice

You already know that db_xref fields stitch records together one hop at a time — this lesson gives you the tools that do the stitching for you, and the mental model for when a "simple" conversion secretly needs several hops.

The problem: everyone names it differently

Say you have a UniProt accession, P38398 (BRCA1_HUMAN), and a script downstream needs a RefSeq protein ID instead. You could open the UniProt entry, scroll to the cross-references section, and copy NP_009225.1 by hand — that works for one record. It falls apart at a hundred. ID mapping is the general task of converting a list of identifiers from one database's namespace into another's, in bulk, without opening a single web page. Two tools do this well: UniProt's own ID-mapping service, and a third-party aggregator called bioDBnet.

UniProt ID Mapping

UniProt runs a mapping service directly off its cross-reference data — the same db_xref-style links you read manually in the db_xref lesson, exposed as a batch API. It lives at rest.uniprot.org/idmapping and works as an asynchronous job: you submit IDs, get back a jobId, then poll until the job finishes.

python
import requests, time

# Step 1: submit the mapping job
r = requests.post(
    "https://rest.uniprot.org/idmapping/run",
    data={"ids": "P38398", "from": "UniProtKB_AC-ID", "to": "RefSeq_Protein"},
)
job_id = r.json()["jobId"]

# Step 2: poll until finished
while True:
    status = requests.get(f"https://rest.uniprot.org/idmapping/status/{job_id}").json()
    if status.get("jobStatus", "FINISHED") == "FINISHED":
        break
    time.sleep(1)

# Step 3: fetch the mapped pairs
results = requests.get(f"https://rest.uniprot.org/idmapping/results/{job_id}").json()
for pair in results["results"][:3]:
    print(pair["from"], "->", pair["to"])
# P38398 -> NP_009225.1
# P38398 -> NP_009228.2
# ...

from and to are namespace names UniProt defines — UniProtKB_AC-ID, RefSeq_Protein, RefSeq_Nucleotide, Ensembl_Transcript, Ensembl_Protein, GeneID, and dozens more. Query rest.uniprot.org/configure/idmapping/fields if you forget the exact spelling of one. Note the shape of the result: P38398 alone returns over twenty NP_* accessions, one per annotated RefSeq transcript for the gene — a single UniProt entry is the canonical protein for a gene, but RefSeq tracks every transcript isoform separately. Mapping is rarely one-to-one; expect a list back, not a single answer.

bioDBnet db2db: many databases in one call

UniProt's mapper only speaks to identifiers UniProt itself cross-references. For a wider net, db2db — a tool hosted by bioDBnet (part of NCI's Advanced Biomedical Computing Center) — accepts one input identifier type and returns several output types in a single request, spanning databases UniProt doesn't directly track (KEGG, HGNC, OMIM, Gene Ontology, and more).

bash
curl "https://biodbnet-abcc.ncifcrf.gov/webServices/rest.php/biodbnetRestApi.json?method=db2db&input=uniprotaccession&inputValues=P38398&outputs=refseqproteinaccession,ensemblgeneid&taxonId=9606"
json
{
  "Input": "UniProt Accession",
  "0": {
    "InputValue": "P38398",
    "outputs": {
      "RefSeq Protein Accession": ["NP_009225", "NP_009228", "..."],
      "Ensembl Gene ID": ["ENSG00000012048"]
    }
  }
}

Two things to note. First, taxonId=9606 (the NCBI Taxonomy ID for human) is required — without it, db2db cannot disambiguate which organism's records to search when an identifier type isn't species-specific. Second, the output is grouped by requested database, each as its own list — you read the JSON differently than UniProt's flat pair list, so budget a few minutes to reshape whichever tool's output you use into your pipeline's format.

UniProt ID MappingbioDBnet db2db
Databases coveredUniProt's own cross-referencesBroader third-party aggregation
Call styleAsync job (submit, poll, fetch)Single synchronous request
Best forAnything already in a UniProt entryKEGG, HGNC, OMIM, and other less-common targets
Species handlingInferred from the entryExplicit taxonId required

Here is the trap. It's tempting to assume any two identifier types can be converted in one hop, because the tools make the request look like one call. They can't always deliver that in one hop internally, and sometimes you have to route it yourself.

Take a concrete case: you have a human Ensembl gene ID and want its mouse ortholog's Ensembl gene ID. There is no direct Ensembl-human-gene-to-Ensembl-mouse-gene cross-reference sitting in either database — orthology isn't a same-namespace lookup, it's a biological inference computed separately. The real path looks like this:

Ensembl human gene (ENSG00000012048)
        |
        v
   NCBI Gene ID (672)              <- Ensembl cross-references NCBI Gene
        |
        v
Ensembl Compara orthology call     <- orthology is computed and stored here,
                                       keyed on Gene ID, not on Ensembl ID directly
        |
        v
   Mouse NCBI Gene ID (12189)
        |
        v
Mouse Ensembl gene ID (ENSMUSG00000017146)

Neither UniProt ID Mapping nor db2db will hand you that whole chain from a single from/to pair, because the intermediate step — the orthology call itself — lives in a different kind of resource (Ensembl's Compara orthology database), not in a simple cross-reference table. You have to know the chain exists and route through GeneID as the pivot.

Whenever a one-hop mapping comes back empty or looks wrong, don't assume the tool is broken — assume there's a missing intermediate. Route through GeneID (NCBI Gene) or a UniProt accession as the pivot; both are cross-referenced almost everywhere, which is exactly why they work as hubs.

Worked example: P38398 to RefSeq to Ensembl

Chaining two mapping calls reproduces the whole cross-database picture for BRCA1 in a few lines:

python
import requests, time

def uniprot_map(ids, from_db, to_db):
    r = requests.post("https://rest.uniprot.org/idmapping/run",
                       data={"ids": ids, "from": from_db, "to": to_db})
    job_id = r.json()["jobId"]
    while requests.get(f"https://rest.uniprot.org/idmapping/status/{job_id}").json().get("jobStatus", "FINISHED") != "FINISHED":
        time.sleep(1)
    return requests.get(f"https://rest.uniprot.org/idmapping/results/{job_id}").json()["results"]

# UniProt -> RefSeq protein
refseq = uniprot_map("P38398", "UniProtKB_AC-ID", "RefSeq_Protein")
print(refseq[0])  # {'from': 'P38398', 'to': 'NP_009225.1'}

# UniProt -> Ensembl transcript (a separate mapping, not derived from the RefSeq step)
ensembl = uniprot_map("P38398", "UniProtKB_AC-ID", "Ensembl_Transcript")
print([p for p in ensembl if p["to"].startswith("ENST00000357654")])
# [{'from': 'P38398', 'to': 'ENST00000357654.9'}]

NM_007294 (RefSeq) and ENST00000357654 (Ensembl) are matched as the community-agreed default transcript for BRCA1 — a pairing scheme called MANE Select, covered in the next lesson. Both mapping calls converging on that same molecule is a useful sanity check that your mapping pipeline is behaving. Verifying that convergence, using MANE as ground truth, is exactly what the Reproducibility Capstone has you script end to end.

A quick sanity habit

Before trusting any mapped ID in a pipeline, spot-check one result manually against the source database's own web page — the five minutes it costs is cheaper than propagating a wrong isoform through downstream analysis. For BRCA1, that means confirming NP_009225.1 really is the MANE Select protein, not just a RefSeq protein among the twenty-plus isoforms UniProt lists. Mapping tools return everything that matches; picking the canonical one is still your job.

Next, we look at how MANE formalizes that "canonical" choice so you don't have to eyeball it every time — see MANE: One Gene, One Agreed Transcript.