# Gene ID Conversion in RNA-seq

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 optionally `AnnotationHub` and `rtracklayer`
- The gene identifiers your pipeline emitted (for example the `row.names` of a `DESeq2` results 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, giving `ENSG00000012048.18`. The version tracks the annotation of that gene model, not the gene itself.
- **Entrez Gene ID.** A number maintained by NCBI, for example `672` for 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

![Four ordered steps for cleaning Ensembl IDs, converting with bitr, merging with all.x true, and running enrichment.](/article-images/illustrations/gene-id-conversion-rna-seq-pathway-analysis-conversion-steps.webp)

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.

```r
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.

```r
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:

```r
      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

```r
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:

```r
sum(is.na(res$SYMBOL))
```

### Step 4. Run pathway analysis in the converted space

For KEGG, which indexes by Entrez ID:

```r
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.

```r
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.

```r
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.

```r
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:

1. Keep Ensembl or Entrez as the key through differential expression.
2. Convert only at the reporting and pathway-analysis stage.
3. 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.

```r
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.

1. **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.
2. **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.
3. **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.
4. **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.
5. **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.
6. **Losing unmapped genes silently.** Every default join in R drops non-matches. Merging with `all.x = TRUE` and counting the `NA`s costs one line and prevents a silent loss that biases enrichment.
7. **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.
8. **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

![Triage path checking version suffixes, species OrgDb, and annotation release when the mapping rate is low.](/article-images/illustrations/gene-id-conversion-rna-seq-pathway-analysis-low-mapping-triage.webp)

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.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "Should I convert gene IDs before or after differential expression?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "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."
      }
    },
    {
      "@type": "Question",
      "name": "Why do I get duplicate gene symbols after conversion?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "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 documented rule such as keeping the gene with maximum mean expression."
      }
    },
    {
      "@type": "Question",
      "name": "What should I do with genes that fail to map?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "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."
      }
    },
    {
      "@type": "Question",
      "name": "Which gene ID do I need for KEGG pathway analysis?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "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."
      }
    },
    {
      "@type": "Question",
      "name": "Can I use gene symbols directly for pathway analysis?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "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."
      }
    },
    {
      "@type": "Question",
      "name": "Why do my Ensembl IDs have a decimal suffix like .18?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "The number after the decimal is the annotation version of that gene model. Strip it with sub before joining to lookup tables that store unversioned accessions, and strip it before differential expression so the ID stays a clean unique key."
      }
    },
    {
      "@type": "Question",
      "name": "How do I convert IDs between human and mouse for cross-species analysis?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "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."
      }
    },
    {
      "@type": "Question",
      "name": "Why does my mapping rate seem low even though the code runs without errors?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "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."
      }
    }
  ]
}
</script>

## Sources

1. [Natural and unanticipated modifiers of RNAi activity in Caenorhabditis elegans.](https://pubmed.ncbi.nlm.nih.gov/23209671/)