Skip to Content
Silent failuresMy gene-set overlap is empty

My overlap with the published gene set is empty

You have 400 differentially expressed genes. The paper you are comparing against has 380. You intersect them and get nine. Both files are valid, the merge ran without complaint, and the number is low enough to be interesting — so you start writing about poor concordance between platforms, when what you have is a list of symbols intersected with a list of Ensembl IDs.

What actually happened

A gene has many names. TP53 is an HGNC symbol; ENSG00000141510 is the Ensembl gene it corresponds to; 7157 is its Entrez ID; P04637 is the UniProt accession of the protein. NM_000546 is a RefSeq transcript. Each is a correct name for the same piece of biology, and none of them matches any of the others as a string.

Nothing in a gene list says which of those spaces it is in. A TSV with a column called id, or gene, or Gene, is a column of text. The space is in the head of whoever exported it, and in the name of the tool that did — featureCounts gives you Ensembl IDs, a GEO supplementary table usually gives you symbols, a proteomics search gives you accessions.

So the join is a string comparison between two different naming conventions. It does not fail. A join of two columns with nothing in common returns the rows they have in common, which is none of them, and an empty result is a perfectly ordinary thing for a join to produce.

Why the answer is plausible rather than absurd

If the overlap were exactly zero, you would notice. It usually is not.

Three things put a handful of rows in it. Some identifiers are valid in both spaces: a symbol like C1QA and an Entrez ID are never confused, but short numeric symbols and numeric IDs can collide. Some lists are already mixed, because they were assembled from two sources, so a fraction of them match. And 2-Sep is in neither space and matches nothing anywhere.

The result is a small, non-zero overlap. That is the worst possible output: it looks like a measurement. A zero would have sent you looking for a bug.

The spreadsheet makes it worse

Open a list of gene symbols in Excel and it converts some of them to dates. SEPT7 becomes 2-Sep. MARCH1 becomes 1-Mar. DEC1, OCT4, MAR1 go the same way. Save, and the symbols are gone — not flagged, not quoted, replaced.

This is not a rare accident. It has been measured across the supplementary files of published genomics papers repeatedly, at rates high enough that HGNC eventually renamed the affected gene families to stop it happening. The corrupted rows are then absent from every join for the rest of the analysis, and because they are absent rather than wrong, nothing downstream has anything to object to.

Why nothing crashed

A merge in pandas, in R, in SQL, or in a shell join is a comparison of strings. None of those tools has a concept of an identifier space, so none of them can tell you that the two columns are named in different ones. They are not being careless; they have no basis on which to object.

The same is true of the step after the join. A hypergeometric enrichment test handed nine overlapping genes computes a p-value for nine overlapping genes. It cannot know that the number should have been two hundred.

What Ligase says

The identifier space is a dimension. A gene list declares it, or the loader reads it off the identifiers themselves where they agree unambiguously — and two lists in different spaces are refused before anything runs.

DimensionErrorin stepscoreligase check — nothing ran
truth.identifier_namespace=HGNC symboldeclared
found.identifier_namespace=Ensembl genefrom the identifiers' own pattern: crates/ligase-core/tests/fixtures/entities_ensembl.tsv
These must match. Add a `map_identifiers` step; identifiers are never renamed implicitly.

Three things are worth noticing.

Neither side was declared. Both say from the identifiers' own pattern, naming the file: every identifier in the first matches the shape of an HGNC symbol and nothing more specific, every identifier in the second matches ENSG followed by eleven digits, so the loader filled the dimension in and said where it got it. You can open the file it names and check.

The two spaces are printed in full, under the names the step binds them to. It says HGNC symbol and Ensembl gene, not “identifier mismatch”.

And the closing question sends you to a step, not to a setting: add a map_identifiers step; identifiers are never renamed implicitly. There is no flag that makes this join work, because going from one space to another is a lookup that can fail, and the engine will not do a lookup behind your back and keep the failures to itself. That is D38.

How it is caught

Load each list as an EntitySet. The Ensembl side settles itself — ENSG is a marker no other space has — and the symbol side says so, because bare symbols are the shape of several spaces at once and the engine will not pick between them (D56):

import ligase as lg mine = lg.entity_set( "results/degs.tsv", organism="Homo sapiens", identifier_namespace="HGNC symbol", ) published = lg.entity_set("truth/published_set.tsv", organism="Homo sapiens") # Nothing has run. This reads the two files' identifier columns. lg.check_step("score", {"truth": published, "found": mine})

That refuses with the message above.

Leave the symbol side undeclared and the refusal is the other one — the dimension is unknown, and two lists that each say nothing are not allowed to agree by silence:

UndeclaredDimensionErrorin stepscoreligase check — nothing ran
truth.identifier_namespace is unknown (not declared, and the identifiers are bare gene-symbol shapes -- symbols and several synonym spaces are indistinguishable by pattern, so declare identifier_namespace)
found.identifier_namespace is unknown (not declared, and the identifiers are bare gene-symbol shapes -- symbols and several synonym spaces are indistinguishable by pattern, so declare identifier_namespace)
`score` needs identifier_namespace to agree, and nothing states it. Declare it where you load the data.
To get the join you wanted, convert — which is one step, and which tells you what it could not convert:

hgnc_to_ensembl = lg.entity_set( "refs/hgnc_to_ensembl.tsv", organism="Homo sapiens", identifier_namespace="HGNC symbol", release="HGNC 2026-09", ) in_ensembl = lg.map_identifiers( mine, hgnc_to_ensembl, column="ensembl_gene_id", to="Ensembl gene", )

in_ensembl declares Ensembl gene, so it joins published and the check passes. Every symbol that did not cross — one the table has no row for, one it maps to several genes — is named in the run report rather than quietly left out. See crossing identifier spaces for the whole worked example.

For why a space is a dimension at all, see identifier spaces; for how it sits alongside the others, the dimension catalogue.

Last updated on