Gene symbol alias resolution: prefer official HGNC, record provenance and which alias was used
- Dominant language
- Python
- Stars
- 30
- Forks
- 3
- Avg merge
- 9h 28m
- Merged PRs (30d)
- 42
Description
🤖 Written by Claude
## Problem
#1667 was a specific case of a general problem: `HGNCMatcher.match_hgnc` walked a `GeneSymbolAlias` row before checking whether the queried string was itself a currently-approved HGNC symbol, so 50 approved symbols resolved to `None` and 830 resolved to a *different gene* (`AIP`→AURKAIP1, `ADA2`→TADA2A, `ACAT1`→SOAT1, `AGT`→AGXT).
That fix was local to `HGNCMatcher`. Alias resolution happens at roughly nine other call sites, each rolling its own, with no shared notion of which alias to prefer and no record of the hop taken.
### 1. No shared resolver, and inconsistent ordering
| Call site | Symbol-first? |
|---|---|
| `genes/gene_matching.py` `HGNCMatcher.match_hgnc` | yes (addressed under #1667) |
| `genes/gene_matching.py:36` `GeneSymbolMatcher.get_gene_symbol_id_and_alias_id` | yes |
| `genes/models.py:319` `GeneSymbolAlias.get_upper_case_lookup` | n/a — collapses all sources to one dict, arbitrary winner |
| `genes/models.py:2740` `MANE.get_mane_and_aliases_list_from_symbol` | no |
| `genes/signals/gene_symbol_search.py:29` | no |
| `classification/models/condition_text_matching.py:324` | no |
| `classification/signals/classification_hooks_grouping_search_terms.py:47` | no |
| `classification/views/exports/classification_export_filter.py:240` | no |
| `genes/models.py:339` `GeneSymbolAliasesMeta` | bidirectional; has the `different_genes` safety check |
`get_upper_case_lookup()` applies no source filter at all, so NCBI, UCSC, HGNC and Manual rows compete on equal footing and the winner depends on queryset order.
### 2. No provenance — authoritative renames are indistinguishable from informal synonyms
`genes/cached_web_resource/hgnc.py:104` flattens two different things into one undifferentiated table:
```python
for alias_list in [previous_symbols, alias_symbols]:
```
`prev_symbol` is an authoritative HGNC rename. `alias_symbol` is an informal synonym list that frequently contains *another gene's* approved symbol. Of the 830 mis-redirects found in #1667, **500 came via informal `alias_symbol` entries** and 327 via `prev_symbol`. Nothing downstream can tell them apart, so nothing can prefer the authoritative one.
### 3. HGNC alias rows are never pruned
`bulk_create(..., ignore_conflicts=True)` with no delete, so every row from every import survives forever. NCBI is handled the other way — `genes/cached_web_resource/refseq.py:37` deletes `source=NCBI` before reimport. The HGNC path should be equally self-correcting.
### 4. What we did isn't recorded, except in two places
`GeneListGeneSymbol.gene_symbol_alias` (`genes/models.py:1960`) and `MANE.get_mane_and_aliases_list_from_symbol` already return/store the alias that was traversed, and `GeneSymbolAliasSummary.match_info` already renders it as "X is an alias for Y (HGNC)". The pattern exists but isn't general — most consumers silently substitute one symbol for another with no audit trail.
## Current data (46,931 HGNC rows, imported 2026-07-30)
- 61,596 HGNC alias rows + 535 NCBI = 59,588 distinct aliases
- 2,043 aliases have more than one distinct target — resolved arbitrarily
- 880 aliases are themselves currently-approved HGNC symbols
- 0 cross-source conflicts today, because `refseq.py:35` only adds NCBI aliases for symbols not already known. Source priority is therefore currently latent rather than actively broken — worth designing in, not urgent on its own.
## Proposal
1. **One resolver.** A single entry point that all call sites use, ordered: currently-approved HGNC symbol → HGNC `prev_symbol` rename → HGNC `alias_symbol` synonym → NCBI → Manual. Returns the matched symbol *and* the alias hop taken (or `None`), so callers can record it.
2. **Record provenance on `GeneSymbolAlias`.** Distinguish authoritative rename from informal synonym, so step 1 can order them. Either a new field set by `save_hgnc_records`, or splitting `GeneSymbolAliasSource.HGNC` into two values.
3. **Make the HGNC import self-correcting**, matching the NCBI pattern, so retired rows disappear on reimport.
4. **Generalise recording the hop**, following `GeneListGeneSymbol.gene_symbol_alias` and `GeneSymbolAliasSummary.match_info`, so a substitution is always visible to the user rather than silent.
5. **Carry `different_genes` into the shared resolver** — `GeneSymbolAliasesMeta` already computes this safety check and it's the strongest signal available for whether a substitution is legitimate.
Ordering matters most where a substitution feeds something clinical — condition text matching and classification export filters both traverse aliases with no ordering today.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by tracing the listed alias call sites in genes/gene_matching.py, genes/models.py, genes/signals/gene_symbol_search.py, classification/models/condition_text_matching.py, classification/signals/classification_hooks_grouping_search_terms.py, and classification/views/exports/classification_export_filter.py. Compare the HGNC import in genes/cached_web_resource/hgnc.py with the NCBI import, then define the shared resolver and provenance behavior. Done means the call sites use consistent priority, HGNC rows self-correct, different_genes safety is retained, and alias hops remain visible to consumers.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend, databases
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100