Gene names: old identifiers and Excel errors

SeqoutGeneNames() converts gene IDs to names using genevintage. SeqoutCorrectNames() repairs gene symbols that Excel changed into dates. Both accept the result of SeqoutMatrix() or a numeric matrix with genes on rows. They preserve expression values and row order, keep feature metadata aligned, and record the original labels and conversion status.

Install seqout and its genevintage dependency from Saket Lab’s r-universe:

install.packages("seqout", repos = c(
  "https://saketlab.r-universe.dev", "https://cloud.r-project.org"
))

The full-data commands below download GEO files and annotation tables on first use. The evaluated examples use small, unaltered excerpts bundled with seqout, so they also run offline. Their source URLs and extraction details are in extdata/gene-names/README.md inside the installed package.

Old Ensembl IDs from a 2014 single-cell experiment

GSE52529, the Monocle myoblast differentiation study, became public on 23 March 2014. Its single-cell file has 47,192 gene rows and 372 cells, with the original versioned Ensembl IDs such as ENSG00000000003.10. The values are FPKM, not raw counts. Select GSE52529_fpkm_matrix.txt.gz; the separate truseq file contains bulk measurements.

old <- SeqoutListCounts("GSE52529") |>
  SeqoutMatrix(sample = "GSE52529_fpkm_matrix.txt.gz")
dim(old$X)
head(rownames(old$X))

# automatic selection; decision recorded below
automatic <- old |> SeqoutGeneNames()
automatic$gene_annotation[[1]]$annotation

Automatic release selection is a ranked estimate: filtered gene sets can fit several references, adjacent releases can share identical gene sets, and versioned IDs do not uniquely identify the annotation used. For this file the detector selects GENCODE 17 on GRCh37, matching the sample’s processing record. You do not need to supply the release or assembly. All 47,192 deposited IDs, including their version suffixes, match that reference exactly.

head(SeqoutGeneReport(automatic))
stopifnot(
  identical(unname(old$X), unname(automatic$X)),
  identical(rownames(automatic$X), rownames(automatic$var))
)

Ranking releases needs no annotation download. It uses the complete original ID vector, bundled separately from the expression excerpt, because ranking only the displayed genes would change the evidence.

id_file <- gzfile(file.path(example_dir, "GSE52529-gene-ids.txt.gz"))
old_ids <- readLines(id_file)
close(id_file)
head(genevintage::detect_release(old_ids), 3)
#>        species  source release assembly feasible   dist n_index
#> 1 homo_sapiens gencode      17       37     TRUE 0.2023   57232
#> 2 homo_sapiens gencode      18       37     TRUE 0.2078   57396
#> 3 homo_sapiens gencode      19       37     TRUE 0.2169   57773

Update genevintage if your installation still ranks release 25 first for this file.

The first 12 genes and first three cells of the deposited file, converted with the matching GENCODE 17 excerpt:

old_excerpt <- as.matrix(read_example("GSE52529-excerpt.tsv.gz", row.names = 1))
gencode17 <- read_example("gencode17-excerpt.tsv.gz")
old_named <- SeqoutGeneNames(old_excerpt, mapping = gencode17)
SeqoutGeneReport(old_named)[, c("input", "name", "status")]
#>                 input     name status
#> 1  ENSG00000000003.10   TSPAN6 mapped
#> 2   ENSG00000000005.5     TNMD mapped
#> 3   ENSG00000000419.8     DPM1 mapped
#> 4   ENSG00000000457.8    SCYL3 mapped
#> 5  ENSG00000000460.12 C1orf112 mapped
#> 6   ENSG00000000938.8      FGR mapped
#> 7  ENSG00000000971.11      CFH mapped
#> 8   ENSG00000001036.8    FUCA2 mapped
#> 9   ENSG00000001084.6     GCLC mapped
#> 10 ENSG00000001167.10     NFYA mapped
#> 11 ENSG00000001460.13    STPG1 mapped
#> 12 ENSG00000001461.12   NIPAL3 mapped
stopifnot(identical(as.vector(old_excerpt), as.vector(old_named)))

Version suffixes are ignored for matching but retained in the audit. For example, ENSG00000000003.10 becomes TSPAN6. An ID without a name in the selected annotation stays as its input ID. Duplicate symbols receive distinct row labels; the audit’s name column preserves the unsuffixed symbol. No rows are summed.

Correct Excel-damaged names in a real deposited matrix

GSE101942 has Excel-damaged gene names. We verified that the original GSE101942_IPSC_rawCounts.txt.gz has 56,637 rows (five of them counting-summary rows) and six samples, with 1-Mar, 9-Sep, and 15-Sep among the row labels.

Select the raw-count file explicitly; the study also deposits differential expression result tables, which are not count matrices.

damaged <- SeqoutListCounts("GSE101942") |>
  SeqoutMatrix(sample = "GSE101942_IPSC_rawCounts.txt.gz")

# explicit target, not automatic
repaired <- damaged |> SeqoutCorrectNames(species = "human", release = 116)
report <- SeqoutGeneReport(repaired)
report[report$status != "unchanged", c("input", "name", "status")]
stopifnot(identical(unname(damaged$X), unname(repaired$X)))

Against Ensembl 116, 25 labels are corrected. 15-Sep stays unresolved because this annotation has no matching candidate for it.

The bundled excerpt contains all 26 damaged labels plus TP53 as an unchanged control, with the original six sample columns and their counts. The annotation excerpt spans the reference’s complete month-related candidate vocabulary.

excel_excerpt <- as.matrix(read_example("GSE101942-excerpt.tsv.gz", row.names = 1))
human116 <- read_example("ensembl116-excel.tsv.gz")
excel_fixed <- SeqoutCorrectNames(excel_excerpt, mapping = human116, quiet = TRUE)
excel_report <- SeqoutGeneReport(excel_fixed)
excel_report[
  excel_report$input %in% c("1-Mar", "9-Sep", "15-Sep", "TP53"),
  c("input", "name", "status")
]
#>     input    name     status
#> 2   1-Mar MARCHF1  corrected
#> 13 15-Sep  15-Sep unresolved
#> 26  9-Sep SEPTIN9  corrected
#> 27   TP53    TP53  unchanged
table(excel_report$status)
#> 
#>  corrected  unchanged unresolved 
#>         25          1          1
stopifnot(identical(as.vector(excel_excerpt), as.vector(excel_fixed)))

Historical versus newer symbols

The same damaged labels resolve differently against human Ensembl 95 and 116. This compares two reference vocabularies; it does not infer which release GSE101942 used.

human95 <- read_example("ensembl95-excel.tsv.gz")
excel_old <- SeqoutCorrectNames(excel_excerpt, mapping = human95, quiet = TRUE)
old_report <- SeqoutGeneReport(excel_old)
show <- excel_report$input %in% c("1-Mar", "9-Sep")
data.frame(
  deposited = excel_report$input[show],
  ensembl95 = old_report$name[show],
  ensembl116 = excel_report$name[show]
)
#>   deposited ensembl95 ensembl116
#> 1     1-Mar    MARCH1    MARCHF1
#> 2     9-Sep     SEPT9    SEPTIN9

1-Mar resolves to MARCH1 in Ensembl 95 and MARCHF1 in 116; 9-Sep to SEPT9 and SEPTIN9. Names alone cannot tell seqout which species or vintage to choose, so Excel correction requires either an annotation table or both species and release. Ambiguous dates are retained and marked ambiguous; tokens without candidates are marked unresolved. genevintage cannot repair some formats or losses of numeric precision. Review the report before analysis, and correct only columns known to hold gene labels.

If the damaged symbols are in feature metadata, pass that column explicitly:

repaired <- SeqoutCorrectNames(
  rna,
  ids = rna$var$symbol, species = "human", release = 116
)

The original var$symbol column is retained. The repaired labels become the row names of both X and var, and each call appends its audit to gene_annotation. Save the corrected seqout_matrix if you need to retain this audit alongside a later Seurat or SingleCellExperiment conversion.