extract_cmap_data_from_siginfo(landmark = FALSE) fails on LINCS2020 geneinfo: duplicate 'row.names' are not allowed
Summary
extract_cmap_data_from_siginfo() labels the rows of the extracted matrix with
geneinfo$gene_symbol. That column is unique within each feature_space, but not across the
full 12,328-feature space: MIA2 appears twice in the official geneinfo_beta.txt. Since the
symbols are assigned as data.frame row names, landmark = FALSE fails outright on the canonical
reference file:
Error in `.rowNamesDF<-`(x, value = value) : duplicate 'row.names' are not allowed
landmark = TRUE is unaffected, which is why the README quick-start and the Step 3 example both
work — they pass landmark = TRUE.
Reproduction
Self-contained and runs in seconds. It needs only geneinfo_beta.txt (1.1 MB); the GCTX is stubbed
out because the bug is in row labelling and never touches matrix values.
library(CONCERTDR)
gi_url <- "https://s3.amazonaws.com/macchiato.clue.io/builds/LINCS2020/geneinfo_beta.txt"
gi_file <- file.path(tempdir(), "geneinfo_beta.txt")
if (!file.exists(gi_file)) download.file(gi_url, gi_file, quiet = TRUE)
gi <- read.delim(gi_file, stringsAsFactors = FALSE, quote = "", comment.char = "")
sum(duplicated(gi$gene_symbol))
#> 1
gi[gi$gene_symbol == "MIA2", c("gene_id", "ensembl_id", "gene_symbol", "feature_space")]
#> gene_id ensembl_id gene_symbol feature_space
#> 2154 117153 ENSG00000150526 MIA2 inferred
#> 5100 4253 ENSG00000150527 MIA2 best inferred
# Minimal stand-in for level5_beta_*.gctx: real gene_ids, four made-up signatures.
si <- data.frame(sig_id = paste0("S", 1:4), pert_id = paste0("BRD-X", 1:4),
cmap_name = paste0("d", 1:4), pert_type = "trt_cp", cell_iname = "THP1",
pert_idose = "10 uM", pert_itime = "24 h", is_hiq = 1L,
stringsAsFactors = FALSE)
g <- file.path(tempdir(), "mini.gctx"); unlink(g)
rhdf5::h5createFile(g)
for (p in c("/0","/0/DATA","/0/DATA/0","/0/META","/0/META/ROW","/0/META/COL"))
rhdf5::h5createGroup(g, p)
rhdf5::h5write(matrix(0, nrow(gi), 4), g, "/0/DATA/0/matrix")
rhdf5::h5write(as.character(gi$gene_id), g, "/0/META/ROW/id")
rhdf5::h5write(si$sig_id, g, "/0/META/COL/id")
rhdf5::h5closeAll()
run <- function(landmark) tryCatch(
sprintf("OK - %d genes", nrow(extract_cmap_data_from_siginfo(
siginfo_file = si, geneinfo_file = gi, gctx_file = g,
filter_quality = FALSE, landmark = landmark, verbose = FALSE))),
error = function(e) paste("ERROR:", conditionMessage(e)))
run(TRUE)
#> [1] "OK - 978 genes"
run(FALSE)
#> [1] "ERROR: duplicate 'row.names' are not allowed"
The same failure occurs with the real level5_beta_trt_cp_n720216x12328.gctx; the stub only makes
it fast to demonstrate.
Cause
get_rid() returns gene_symbol as genenames for every row of geneinfo, and
extract_cmap_data_from_siginfo() then does:
expression_data <- as.data.frame(mat)
rownames(expression_data) <- genenames # <- fails when genenames has a duplicate
MIA2 is attached to two distinct features — Entrez 117153 / ENSG00000150526 ("melanoma inhibitory
activity 2", inferred) and Entrez 4253 / ENSG00000150527 ("MIA SH3 domain ER export factor 2",
best inferred) — a leftover of the NCBI MIA2/CTAGE5 reorganisation.
Why it matters
landmark = FALSE is the only usable setting for a small query signature, so this blocks the
package for that whole class of analysis rather than being a corner case. For a 168-gene
differential-expression signature we measured:
| reference space |
features |
query genes covered |
landmark = TRUE |
978 |
13 / 168 |
landmark = FALSE |
12,328 |
113 / 168 |
13 genes is not enough signature to score, so the full feature space is required — and it is
exactly the path that errors.
Suggested fix
Make gene_symbol unique before it is used as row names, preferring the better-annotated feature
(landmark > best inferred > inferred) rather than dropping arbitrarily:
rank_space <- c(landmark = 1L, "best inferred" = 2L, inferred = 3L)
pref <- rank_space[geneinfo_df$feature_space]
pref[is.na(pref)] <- 4L
keep <- !duplicated(geneinfo_df$gene_symbol[order(pref)])[order(order(pref))]
geneinfo_df <- geneinfo_df[keep, ]
Verified against the reproduction above: landmark = FALSE then returns 12,327 genes. A warning
naming what was dropped would help, since users should know a feature disappeared.
make.unique(genenames) also stops the error but is worse: it renames the feature to MIA2.1,
where no query gene can ever match it, so the row is silently dead weight rather than absent.
Two smaller observations from the same code path
Happy to split these into their own issues if you'd prefer.
-
The row order of geneinfo does not match GCTX row order, so the index passed to
rhdf5::h5read() is unsorted — for the landmark subset as well as the full space. The first few
geneinfo features sit at GCTX rows 9675, 8434, 9676, 11053, … Labelling is correct today,
because this rhdf5 returns rows in the order requested rather than sorted order; I verified that
against rows read one at a time from the real GCTX, for both landmark settings. But it is an
unstated dependency on rhdf5's semantics, and if it ever changed the result would be a silently
mislabelled matrix rather than an error. Sorting the requested index (and labelling from the
same order) would remove the dependency.
-
annotate_drug_results() keeps only the first compoundinfo row per pert_id
(compinfo <- compinfo[!duplicated(compinfo$pert_id), , drop = FALSE]), so a multi-target
compound loses its other targets. That may well be deliberate, but it does discard annotation
that matters if the target column is used for target-level aggregation downstream.
Environment
CONCERTDR 0.99.2 (GithubSHA1 d27d37cc7cfff088069a80ddad5482a20923cc08)
R version 4.6.1 (2026-06-24), x86_64-conda-linux-gnu, Rocky Linux 9.8
rhdf5 and data.table from conda-forge
CMap files: LINCS2020 geneinfo_beta.txt (12,328 rows, dated 2020-12-17)
extract_cmap_data_from_siginfo(landmark = FALSE)fails on LINCS2020 geneinfo:duplicate 'row.names' are not allowedSummary
extract_cmap_data_from_siginfo()labels the rows of the extracted matrix withgeneinfo$gene_symbol. That column is unique within eachfeature_space, but not across thefull 12,328-feature space:
MIA2appears twice in the officialgeneinfo_beta.txt. Since thesymbols are assigned as
data.framerow names,landmark = FALSEfails outright on the canonicalreference file:
landmark = TRUEis unaffected, which is why the README quick-start and the Step 3 example bothwork — they pass
landmark = TRUE.Reproduction
Self-contained and runs in seconds. It needs only
geneinfo_beta.txt(1.1 MB); the GCTX is stubbedout because the bug is in row labelling and never touches matrix values.
The same failure occurs with the real
level5_beta_trt_cp_n720216x12328.gctx; the stub only makesit fast to demonstrate.
Cause
get_rid()returnsgene_symbolasgenenamesfor every row ofgeneinfo, andextract_cmap_data_from_siginfo()then does:MIA2is attached to two distinct features — Entrez 117153 / ENSG00000150526 ("melanoma inhibitoryactivity 2",
inferred) and Entrez 4253 / ENSG00000150527 ("MIA SH3 domain ER export factor 2",best inferred) — a leftover of the NCBI MIA2/CTAGE5 reorganisation.Why it matters
landmark = FALSEis the only usable setting for a small query signature, so this blocks thepackage for that whole class of analysis rather than being a corner case. For a 168-gene
differential-expression signature we measured:
landmark = TRUElandmark = FALSE13 genes is not enough signature to score, so the full feature space is required — and it is
exactly the path that errors.
Suggested fix
Make
gene_symbolunique before it is used as row names, preferring the better-annotated feature(
landmark>best inferred>inferred) rather than dropping arbitrarily:Verified against the reproduction above:
landmark = FALSEthen returns 12,327 genes. A warningnaming what was dropped would help, since users should know a feature disappeared.
make.unique(genenames)also stops the error but is worse: it renames the feature toMIA2.1,where no query gene can ever match it, so the row is silently dead weight rather than absent.
Two smaller observations from the same code path
Happy to split these into their own issues if you'd prefer.
The row order of
geneinfodoes not match GCTX row order, so the index passed torhdf5::h5read()is unsorted — for the landmark subset as well as the full space. The first fewgeneinfofeatures sit at GCTX rows 9675, 8434, 9676, 11053, … Labelling is correct today,because this rhdf5 returns rows in the order requested rather than sorted order; I verified that
against rows read one at a time from the real GCTX, for both
landmarksettings. But it is anunstated dependency on rhdf5's semantics, and if it ever changed the result would be a silently
mislabelled matrix rather than an error. Sorting the requested index (and labelling from the
same order) would remove the dependency.
annotate_drug_results()keeps only the firstcompoundinforow perpert_id(
compinfo <- compinfo[!duplicated(compinfo$pert_id), , drop = FALSE]), so a multi-targetcompound loses its other targets. That may well be deliberate, but it does discard annotation
that matters if the
targetcolumn is used for target-level aggregation downstream.Environment