Gene ID Conversion in RNA-seq
By Dr. Zubair Khalid, DVM, MS, PhD ·

By the end of this guide you will be able to take a gene list produced by an RNA-seq pipeline, convert it into the identifier type your pathway analysis tool expects, handle duplicate and unmapped genes correctly, and diagnose the error messages that appear when the conversion goes wrong. You will have working R code for four conversion strategies, a decision table that tells you which identifier to feed each popular pathway database, and a short validation routine that catches silent failures before they reach a figure.
What you need on hand before starting:
- R (4.2 or newer) with the packages
clusterProfiler,org.Hs.eg.db(or the OrgDb for your species),biomaRt,gprofiler2, and optionallyAnnotationHubandrtracklayer - The gene identifiers your pipeline emitted (for example the
row.namesof aDESeq2results object) - The GTF or GFF annotation file and genome build you used for alignment and quantification
- A plain text editor for inspecting files. Do not use spreadsheet software to edit count tables, for reasons covered below
The identifier landscape in RNA-seq
Every RNA-seq analysis produces identifiers, and pathway tools consume identifiers, but the two rarely speak the same dialect. Before converting anything, you need to know what you are holding.
The main identifier types
- Ensembl gene ID. A stable accession such as
ENSG00000012048. Newer Ensembl releases append a version number, givingENSG00000012048.18. The version tracks the annotation of that gene model, not the gene itself. - Entrez Gene ID. A number maintained by NCBI, for example
672for BRCA1. KEGG, and most tools built on KEGG, index pathways by Entrez ID. - HGNC gene symbol. A short human-readable name such as
BRCA1. Symbols are what humans want to see in figures, but they change over time and some symbols map to more than one gene. - RefSeq accession. Transcript-level accessions such as
NM_007294. These describe individual transcript isoforms. - UniProt accession. Protein-level identifiers such as
P38398. Common in proteomics data and in some interaction databases.
A single gene can carry all of these at once. BRCA1 is ENSG00000012048, Entrez 672, symbol BRCA1, and UniProt P38398. Conversion is simply joining your list to a lookup table that connects these accessions, and the entire class of problems known as gene ID conversion failures comes from joining against the wrong table, the wrong version of the table, or the wrong column.
Why this step exists at all
Differential expression tools such as DESeq2 and edgeR do not care what your row names are. They count reads per feature and test each one. The identifiers enter with your GTF or reference transcriptome and pass through untouched. Pathway databases, by contrast, were built on a particular identifier type. KEGG pathway maps are keyed on Entrez IDs, the MSigDB gene sets distributed by the Broad Institute are keyed on symbols, and Gene Ontology resources work with whatever ID the underlying annotation table provides. When the identifiers on your DE gene list do not match the identifiers inside the database, the enrichment analysis reports nothing, or worse, reports something based on a small accidental overlap.
Why gene IDs break, and where the errors hide
Three failure modes account for nearly every gene ID conversion problem in RNA-seq work. Knowing them turns bioinformatics troubleshooting from guesswork into a checklist.
Version suffixes and prefixes
Ensembl now emits versioned gene IDs in many GTF files, so a count matrix row might read ENSG00000012048.18 while your lookup table stores ENSG00000012048. An exact string join matches zero rows. Some GTF flavors also carry prefixes such as gene: that must be stripped. This is the single most common reason a conversion returns an empty result while the code runs without complaint.
Identifier drift
Symbols are edited. The HGNC, which curates human gene symbols, renamed the MARCH gene family to MARCHF1 through MARCHF8 in 2021, so a gene set or table built before that date will not match a table built after it. Symbols are also reused across species and across history, and alias lists overlap, which means a symbol-to-anything conversion is inherently the least reliable direction. Ensembl IDs and Entrez IDs are far more stable, though Ensembl retires IDs occasionally when gene models are merged or split.
Build and release mismatch
Your counts were quantified against one annotation. Your lookup table comes from another. Ensembl release 86 for human corresponds to the GRCh37 coordinate system, while current releases use GRCh38, and gene models were renamed, split, and merged between them. If you mapped reads against one build and convert IDs from another, some genes will not convert and a few will convert incorrectly. This is a structural problem, not a bug in your code, and it is fixed by using the annotation you actually quantified against.
A related and underappreciated source of unexplained variation exists even in the cleanest systems. Whole-genome resequencing has substantiated sequence differences between independently maintained wild-type stocks of model organisms, and such variation can enhance or suppress the effects of known mutations in ways that complicate analysis [1]. The practical lesson for an RNA-seq analyst is that two samples labeled identically are not guaranteed to be identical at the sequence level, so a conversion or mapping discrepancy that refuses to resolve may reflect real biological or sample-level differences rather than a code error. The same study documented a de novo mutation in an existing strain that initially frustrated the authors' phenotypic analysis, a useful reminder that not every anomaly traces back to your pipeline [1].
Choosing the target identifier for your pathway tool
Do not convert to a single universal ID. Convert to whatever the downstream tool expects. The table below summarizes the pairing.
| Pathway resource | Preferred input ID | Typical tool | Main pitfall |
|---|---|---|---|
KEGG (via clusterProfiler) | Entrez ID | bitr then enrichKEGG | Numeric IDs mangled by spreadsheet software |
Gene Ontology (via clusterProfiler) | Any ID in the OrgDb, commonly Entrez or SYMBOL | bitr, enrichGO | Wrong species OrgDb silently maps nothing |
Reactome (via ReactomePA) | Entrez ID | bitr then enrichPathway | Same Entrez issues as KEGG |
| MSigDB / GSEA | Gene symbol | Keep symbols, or convert to symbol | Symbol ambiguity and drift |
g:Profiler (gprofiler2) | Most types accepted | gost, gorth | Must declare the input namespace correctly |
Two habits prevent most of the pain. First, carry every identifier through the pipeline together. When your DE result rows have Ensembl IDs, convert once, keep the Ensembl ID as the key, and attach symbol and Entrez as extra columns rather than replacing the key. Second, never convert the list of significant genes alone. Convert the full universe of tested genes, then subset, so your background set for enrichment is defined in the same identifier space as your hits.
Gene ID conversion for RNA-seq pathway analysis, step by step

The following walkthrough assumes a human RNA-seq experiment analyzed with DESeq2, whose results object rows carry Ensembl gene IDs with version suffixes. The same logic transfers to other species by swapping the OrgDb.
Step 1. Clean the identifiers before converting
Strip version suffixes, whitespace, and prefixes so the join keys are exact.
res <- as.data.frame(DESeq2::results(dds))
res$gene_id <- rownames(res)
res$gene_id <- sub("\\..*$", "", res$gene_id)
res$gene_id <- trimws(res$gene_id)
The regular expression \\..*$ removes everything from the first period onward, turning ENSG00000012048.18 into ENSG00000012048. Run head(res$gene_id) and confirm every entry looks like a bare accession.
Step 2. Convert with an OrgDb through clusterProfiler
An OrgDb is an organism-level annotation database package. org.Hs.eg.db covers human, and equivalent packages exist for mouse, rat, zebrafish, and many other organisms.
library(clusterProfiler)
library(org.Hs.eg.db)
mapped <- bitr(res$gene_id,
fromType = "ENSEMBL",
toType = c("SYMBOL", "ENTREZID"),
OrgDb = org.Hs.eg.db)
head(mapped)
Expected output looks like this:
ENSEMBL SYMBOL ENTREZID
1 ENSG00000012048 BRCA1 672
2 ENSG00000141510 TP53 7157
3 ENSG00000146648 EGFR 1956
Two behaviors matter here. bitr drops genes it cannot map and prints the proportion dropped, for example 12.4% of input gene IDs are fail to map. Read that number every time. A few percent is normal for Ensembl IDs against an OrgDb because some Ensembl gene models are not represented. A large percentage means wrong ID type, wrong species, or unmangled version suffixes still present. Second, bitr reports Warning: duplicate ... removed when one input ID maps to multiple targets. Decide what to do about duplicates rather than ignoring the warning. Handling is covered in a dedicated section below.
Step 3. Merge the mapping back onto your results
res <- merge(res, mapped, by.x = "gene_id", by.y = "ENSEMBL", all.x = TRUE)
Using all.x = TRUE keeps unmapped genes in the table with NA symbols, which lets you quantify how many DE genes failed conversion rather than losing them silently. Count them:
sum(is.na(res$SYMBOL))
Step 4. Run pathway analysis in the converted space
For KEGG, which indexes by Entrez ID:
sig <- subset(res, padj < 0.05 & abs(log2FoldChange) > 1)
universe <- res$ENTREZID[!is.na(res$ENTREZID)]
ego <- enrichGO(gene = sig$ENTREZID,
OrgDb = org.Hs.eg.db,
keyType = "ENTREZID",
universe = universe,
ont = "BP",
pAdjustMethod = "BH",
qvalueCutoff = 0.05)
ekegg <- enrichKEGG(gene = sig$ENTREZID,
universe = universe,
organism = "hsa",
pAdjustMethod = "BH")
Passing the universe argument matters. Without it, the enrichment background becomes everything in the database, which inflates significance for gene sets that happen to be well annotated. The correct background is the set of genes that were actually tested in your differential expression analysis, expressed in the same identifier type as your hits.
Step 5. Alternative, biomaRt for full control
biomaRt queries Ensembl directly and lets you pin the release, which solves the build mismatch problem when you know which release you quantified against.
library(biomaRt)
ensembl <- useEnsembl("ensembl",
dataset = "hsapiens_gene_ensembl",
version = 110)
lookup <- getBM(attributes = c("ensembl_gene_id", "hgnc_symbol", "entrezgene_id",
"gene_biotype"),
filters = "ensembl_gene_id",
values = res$gene_id,
mart = ensembl)
The version argument fixes the Ensembl release. If you quantified against a GRCh37-based reference, use the appropriate archived release rather than the current one. biomaRt output can differ from OrgDb output because the sources update on different schedules, and entrezgene_id is sometimes absent for genes without an Entrez record, leaving NA. Both tools are correct. Discrepancies between them are a signal to check versions, not to pick whichever answer you prefer.
Step 6. Alternative, gprofiler2 when the tool accepts many namespaces
gprofiler2::gost maps on the fly and accepts a mixed list if you declare the namespace.
library(gprofiler2)
go_res <- gost(sig$ENTREZID,
organism = "hsapiens",
sources = c("GO:BP", "KEGG", "REAC"),
evcodes = TRUE)
gost handles the conversion internally, which is convenient but removes your visibility into how many genes mapped. For publication work where a reviewer may ask for the mapping rate, the explicit bitr approach is easier to document.
Step 7. Convert at the gene level when starting from transcripts
If you quantified at the transcript level with tools such as Salmon, build a transcript-to-gene table from the same GTF you quantified against and aggregate before conversion.
library(rtracklayer)
gtf <- import("Homo_sapiens.GRCh38.110.gtf")
tx2gene <- unique(mcols(gtf)[mcols(gtf)$type == "transcript",
c("transcript_id", "gene_id")])
The tximport package uses exactly this structure to summarize transcript counts to gene counts, and building it from your own GTF guarantees transcript and gene identifiers come from the same annotation release.
Handling many-to-one and one-to-many mappings
Conversion is not always one-to-one, and how you handle it changes your results.
Many-to-one, several Ensembl IDs per symbol
This happens with paralogs that share a short name, readthrough transcripts, and older symbols. If you collapse by symbol, each row becomes an aggregate, and downstream DE testing on already-aggregated rows is statistically invalid because you are collapsing after testing. The safe order is:
- Keep Ensembl or Entrez as the key through differential expression.
- Convert only at the reporting and pathway-analysis stage.
- If a downstream tool requires symbol-level rows, aggregate expression with a documented rule, commonly taking the gene with the maximum mean expression or the maximum fold change.
agg <- aggregate(cbind(baseMean, log2FoldChange) ~ SYMBOL,
data = res, FUN = max, na.rm = TRUE)
Whichever rule you choose, state it in your methods. Rules that sound arbitrary still beat silent duplication.
One-to-many, one gene to several targets
A single Ensembl ID can map to several Entrez IDs when a gene model spans records. bitr expands such rows, which can double-count a gene in an enrichment analysis. If your hits are few, inspect them manually. If they are many, deduplicate on the key you trust most, usually the Ensembl ID, and keep one target per key.
Why symbols deserve suspicion
Symbols drift and collide. Beyond the MARCHF renaming example above, symbols get retired and reused, alias lists overlap between genuinely different genes, and reading a symbol list from a paper written years earlier means matching against symbols that may no longer be current. Treat any conversion that starts from symbols as lossy, and prefer conversions that start from a stable accession. The biological parallel is instructive. Sequence-level relationships between genes can themselves be reshuffled over evolutionary time through processes such as meiotic gene conversion, which is one of the mechanisms that introduces sequence differences among otherwise related strains [1]. Annotation is a moving map of a moving territory, so lock your map version down and record it.
Troubleshooting exact error messages
When an rna seq error appears, read the message as a diagnostic rather than a nuisance. These are the messages you will actually see during gene ID conversion work.
"Error in .rowNamesDF<- ... duplicate 'row.names' are not allowed" from DESeq2 or related objects. Your conversion produced duplicated row names because several Ensembl IDs collapsed onto one symbol. Fix by deduplicating on a unique key before constructing the object, or revert to the unconverted IDs for the statistical step and convert afterward.
"No gene can be mapped ...." from clusterProfiler. The input ID type does not match what the database expects, or the species is wrong, or version suffixes are still attached. Check the first five entries of your gene vector by eye, confirm the organism package, and re-strip suffixes.
"The query to the BioMart Martservice returned no result" from biomaRt. The mirror is down, the filter value format is wrong, or the values vector is empty. Re-run with a handful of known good IDs such as ENSG00000012048 to separate a service problem from a data problem.
An empty result with no warning at all. The most dangerous case. Your merge with all = FALSE (the default) silently dropped every unmapped gene. Always merge with all.x = TRUE and count the NA rows.
A pathway analysis that succeeds but returns almost nothing. Your gene universe shrank. If conversion dropped 40 percent of genes before enrichment, the background changed and power dropped. Diagnose by comparing the length of the original list against the length of the converted list at every step.
Nonsensical gene symbols appearing in output. If you ever see identifiers that look like dates or scrambled names, suspect spreadsheet software. Opening a count table in Excel can corrupt numeric gene identifiers and some symbols into dates or floating point numbers, a documented and long-running problem in genomics. Keep count tables as CSV, read them directly into R, and never round-trip them through a spreadsheet.
A discrepancy between tools that should agree. OrgDb, Ensembl, and KEGG update on different schedules. Document the package versions with sessionInfo() and the Ensembl release number in your methods so the discrepancy is reproducible rather than mysterious.
Common Mistakes and Limitations
The following mistakes account for most retractions of pathway figures, and most of them produce no error message at all.
- Converting only the significant hits. Your enrichment background becomes the mappable subset of your hits rather than your tested universe. Convert the full gene list first, derive the universe from it, then subset.
- Ignoring the mapping rate. If 30 percent of DE genes fail conversion, something is structurally wrong, most often a build mismatch or the wrong species. Treat a mapping rate below roughly 90 percent for well-annotated organisms as a stop signal and investigate before proceeding.
- Using a different annotation release for conversion than for quantification. Your GTF, your genome build, your transcript-to-gene map, and your lookup table should all come from the same release. Record the release number.
- Collapsing duplicates before testing. Aggregating expression across duplicate symbols before differential expression changes the counts and invalidates the statistical model. Test on stable unique IDs, collapse only for reporting.
- Trusting symbols as keys. Symbols change, collide, and get reused. The MARCH family renaming shows how even a systematic rename breaks joins across time. Convert from symbols only when a database demands it.
- Losing unmapped genes silently. Every default join in R drops non-matches. Merging with
all.x = TRUEand counting theNAs costs one line and prevents a silent loss that biases enrichment. - Not reporting versions. Reproducibility in gene ID conversion is a function of versions. Report the Ensembl release, the OrgDb package version, the genome build, and
sessionInfo()output. - Assuming conversion errors explain everything. Sometimes the anomaly is real biology. Even in isogenic laboratory strains, natural variation and de novo mutations can produce effects that initially look like pipeline failures, as documented in C. elegans where a de novo mutation in an existing strain frustrated the researchers' own phenotypic analysis [1]. When a handful of genes resist every conversion attempt, check whether they represent genuine strain-specific or novel sequence rather than an annotation artifact.
A final limitation belongs in every methods paragraph you write from this workflow. Conversion tables lag the annotation they summarize, and different resources resolve the same gene differently. Your results are conditional on the release you used, and a reviewer should be able to reproduce your exact mapping from the versions you report.
Validating the conversion before you trust it

Three quick checks catch most silent failures.
Spot-check known genes. Keep a short personal reference of verified triples, for example BRCA1 (ENSG00000012048, Entrez 672, symbol BRCA1), TP53 (ENSG00000141510, Entrez 7157), and EGFR (ENSG00000146648, Entrez 1956). After every conversion, confirm these three map correctly. If they do not, the problem is in your code, not your data.
Check the mapping rate per list. Compute the fraction of your DE hits that converted, and separately the fraction of the universe. If hits convert at a much lower rate than the universe, your most interesting genes are systematically the ones being dropped, and your enrichment result is biased.
Back-convert a sample. Take a random 20 genes, convert them to symbols, convert back to Ensembl IDs, and confirm the round trip returns the original set. Round-trip failures expose ambiguous mappings immediately.
Frequently Asked Questions
Should I convert gene IDs before or after differential expression?
After. Run differential expression on the unique, stable identifiers that came with your annotation, then convert for reporting and pathway analysis. Converting first risks duplicate keys and changed gene sets, both of which break the statistical model.
Why do I get duplicate gene symbols after conversion?
Several Ensembl genes can legitimately share a symbol through paralogs, aliases, or readthrough transcripts. Keep one row per stable ID for statistics, and if a tool requires symbol rows, collapse with a rule you document, such as keeping the gene with maximum mean expression.
What should I do with genes that fail to map?
Keep them in your table as NA and count them. A small unmapped fraction is normal. A large one signals wrong ID type, wrong species, or an annotation build mismatch, and must be fixed rather than ignored because dropping those genes biases enrichment results.
Which gene ID do I need for KEGG pathway analysis?
Entrez Gene ID. Tools such as clusterProfiler::enrichKEGG index KEGG by Entrez, so convert your Ensembl IDs to Entrez with bitr before calling the enrichment function.
Can I use gene symbols directly for pathway analysis?
Sometimes, for example with MSigDB gene sets that are keyed on symbols, but symbols are the least stable identifier and can map ambiguously. If the tool supports it, supply Entrez or Ensembl IDs and let the tool convert internally so the mapping is auditable.
Why do my Ensembl IDs have a decimal suffix like .18?
The number after the decimal is the annotation version of that gene model. Strip it with sub("\\..*$", "", ids) before joining to lookup tables that store unversioned accessions, and strip it before differential expression so the ID stays a clean unique key.
How do I convert IDs between human and mouse for cross-species analysis?
That is orthology mapping, a different operation from ID conversion within one species. Use gprofiler2::gorth with source and target organisms, or the orthology tables in biomaRt. Do not simply match symbols across species, because the same short name can refer to different genes in different lineages.
Why does my mapping rate seem low even though the code runs without errors?
A silent non-match is the most common low-mapping-rate cause. Check for version suffixes, prefixes, whitespace, the wrong organism database, and a genome build or Ensembl release that differs between your quantification and your lookup table. Spot-checking a few known gene IDs isolates the cause quickly.