mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-01 01:08:11 +08:00
This splits the backend into two parts: the local backend for desktop cellxgene and the AWS backend for hosted cellxgene. The local backend is in local_server while the hosted remains in server. The general idea is to copy everything from server to local_server, pull unneeded stuff out of local_server, and keep server as-is for this PR. Not touching server means all the infra and deployment code will continue working just as it did before so we can make those changes incrementally.
212 lines
7.7 KiB
Python
212 lines
7.7 KiB
Python
"""Helpers for converting and checking HGNC gene symbols."""
|
|
|
|
import argparse
|
|
import enum
|
|
import logging
|
|
import os
|
|
import re
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
|
|
def get_upgraded_var_index(var, hgnc_path=None):
|
|
"""Given an anndata var dataframe, return a new index for the dataframe
|
|
where human gene symbols have been upgraded to the current HGNC set.
|
|
"""
|
|
|
|
if not hgnc_path:
|
|
hgnc_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "hgnc_complete_set.txt.gz")
|
|
|
|
hgnc_symbol_checker = HGNCSymbolChecker.from_hgnc_records(hgnc_path)
|
|
|
|
return pd.Index([hgnc_symbol_checker.upgrade_symbol(s) for s in var.index])
|
|
|
|
|
|
class SymbolStatus(enum.Enum):
|
|
"""The status of a symbol in the HGNC database.
|
|
|
|
APPROVED: Currently a valid symbol
|
|
WITHDRAWN: A previously approved HGNC symbol for a gene that has since been shown
|
|
not to exist _unless_ that symbol is also approved
|
|
AMBIGUOUS: A symbol that is not approved but is an alias or previous symbol for
|
|
multiple approved symbols
|
|
UPGRADABLE: A symbol that is not approved but unambiguously maps to an approved
|
|
symbol
|
|
UNKNOWN: A symbol that does not appear in HGNC
|
|
"""
|
|
|
|
APPROVED = 1
|
|
WITHDRAWN = 2
|
|
AMBIGUOUS = 3
|
|
UPGRADABLE = 4
|
|
UNKNOWN = 5
|
|
|
|
|
|
class HGNCSymbolChecker:
|
|
"""Handle checking and correcting HGNC symbols."""
|
|
|
|
def __init__(self, approved_symbols, withdrawn_symbols, ambiguous_symbols, symbol_map):
|
|
self.approved_symbols = approved_symbols
|
|
self.withdrawn_symbols = withdrawn_symbols
|
|
self.ambiguous_symbols = ambiguous_symbols
|
|
self.symbol_map = symbol_map
|
|
|
|
def print_symbol_map(self):
|
|
"""Print out a map from old symbol to new symbol."""
|
|
|
|
for symbol_pair in self.symbol_map.items():
|
|
print("\t".join(symbol_pair))
|
|
|
|
def check_symbol(self, symbol):
|
|
"""See if a symbol if approved or something else."""
|
|
if symbol in self.approved_symbols:
|
|
return SymbolStatus.APPROVED
|
|
|
|
if symbol in self.withdrawn_symbols:
|
|
return SymbolStatus.WITHDRAWN
|
|
|
|
if symbol in self.ambiguous_symbols:
|
|
return SymbolStatus.AMBIGUOUS
|
|
|
|
if symbol in self.symbol_map:
|
|
return SymbolStatus.UPGRADABLE
|
|
|
|
return SymbolStatus.UNKNOWN
|
|
|
|
def upgrade_symbol(self, symbol):
|
|
"""Return the approved symbol for the given symbol.
|
|
|
|
If the symbol cannot be upgraded, just return the original symbol.
|
|
"""
|
|
|
|
fixed_symbol, stripped_symbol = format_symbol(symbol)
|
|
|
|
if fixed_symbol in self.approved_symbols:
|
|
return fixed_symbol
|
|
elif fixed_symbol in self.symbol_map:
|
|
return self.symbol_map[fixed_symbol]
|
|
elif stripped_symbol in self.approved_symbols:
|
|
return stripped_symbol
|
|
elif stripped_symbol in self.symbol_map:
|
|
return self.symbol_map[stripped_symbol]
|
|
|
|
return symbol
|
|
|
|
@classmethod
|
|
def from_hgnc_records(cls, hgnc_dataset_path):
|
|
"""Parse a hgnc database download into a HGNCSymbolChecker object."""
|
|
|
|
def all_symbols(record):
|
|
"""Get all the symbols associated with an HGNC record including previous, alias,
|
|
and approved."""
|
|
yield format_symbol(record["symbol"])[0]
|
|
for symbol in alias_and_previous_symbols(record):
|
|
yield symbol
|
|
|
|
def alias_and_previous_symbols(record):
|
|
"""Get alias and previous symbols from an HGNC record."""
|
|
for field in ("alias_symbol", "prev_symbol"):
|
|
if record[field] is not np.nan:
|
|
for symbol in record[field].split("|"):
|
|
yield format_symbol(symbol)[0]
|
|
# Sometimes something like HGNC:1234 appears in datasets, which we
|
|
# want to fix as well.
|
|
yield record["hgnc_id"]
|
|
|
|
hgnc_records = pd.read_csv(hgnc_dataset_path, sep="\t", header=0, low_memory=False).to_dict("records")
|
|
|
|
# Get all symbols that are currently approved.
|
|
approved_symbols = set()
|
|
for record in hgnc_records:
|
|
if record["status"] == "Approved":
|
|
approved_symbols.add(format_symbol(record["symbol"])[0])
|
|
|
|
# Get all symbols that have been withdrawn
|
|
withdrawn_symbols = set()
|
|
for record in hgnc_records:
|
|
if record["status"] == "Entry Withdrawn":
|
|
for symbol in all_symbols(record):
|
|
withdrawn_symbols.add(symbol)
|
|
|
|
# If a symbol is both approved and withdrawn, be optimistic and call it approved
|
|
logging.warning(
|
|
f"Some symbols are simulaneously withdrawn and approved\n"
|
|
f"We will treat them at approved:\n"
|
|
f"{withdrawn_symbols.intersection(approved_symbols)}"
|
|
)
|
|
withdrawn_symbols = withdrawn_symbols.difference(approved_symbols)
|
|
|
|
# Now try to map from symbols that are not approved but are an alias or previous symbol for an approved symbol
|
|
alias_previous_to_approved = {}
|
|
ambiguous_symbols = set()
|
|
|
|
for record in hgnc_records:
|
|
if record["status"] == "Approved":
|
|
|
|
# The approved symbol is what we'll map to
|
|
approved_symbol = format_symbol(record["symbol"])[0]
|
|
|
|
for symbol in alias_and_previous_symbols(record):
|
|
|
|
# If the alias or previous symbol is also an approved symbol,
|
|
# we'll just leave it alone
|
|
if symbol in approved_symbols:
|
|
continue
|
|
|
|
# If the alias or previous symbol maps to a different approved symbol, mark it as ambiguous
|
|
if symbol in alias_previous_to_approved and alias_previous_to_approved[symbol] != approved_symbol:
|
|
ambiguous_symbols.add(symbol)
|
|
else:
|
|
alias_previous_to_approved[symbol] = approved_symbol
|
|
|
|
# Remove all the ambiguous symbols from the map
|
|
for ambiguous_symbol in ambiguous_symbols:
|
|
alias_previous_to_approved.pop(ambiguous_symbol)
|
|
|
|
return HGNCSymbolChecker(approved_symbols, withdrawn_symbols, ambiguous_symbols, alias_previous_to_approved)
|
|
|
|
|
|
def format_symbol(symbol):
|
|
"""HGNC rules say symbols should all be upper case except for C#orf#. However, case is
|
|
variable in both alias and previous symbols as well as in the symbols we get in
|
|
submissions. So, upper case everything except for the one situation where mixed-case
|
|
is allowed, which are the genes like C2orf157.
|
|
|
|
Also, seurat and scanpy append ".1" or "-1" to duplicated gene names, and these altered
|
|
names persist throughout the life of the object. They won't match against the HGNC database
|
|
and we want to merge them, so we need to strip off the suffix and try matching again.
|
|
|
|
This function takes a symbol and returns the symbol with the fixed case and also with the
|
|
seurat/scanpy suffix stripped off.
|
|
"""
|
|
|
|
match = re.match(r"^(C)(\d+)(orf)(\d+)$", symbol, re.IGNORECASE)
|
|
|
|
if match:
|
|
fixed_case = f"C{match.group(2)}orf{match.group(4)}"
|
|
else:
|
|
fixed_case = symbol.upper()
|
|
|
|
suffix_stripped = re.sub(r"[\.\-]\d+$", "", fixed_case)
|
|
|
|
return fixed_case, suffix_stripped
|
|
|
|
|
|
def main():
|
|
"""When called as main, parse a given hgnc download and print out a map from old to new
|
|
symbol.
|
|
"""
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"hgnc_dataset", help="HGNC dataset tsv, available from www.genenames.org/download/statistics-and-files/"
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
hgnc_symbol_checker = HGNCSymbolChecker.from_hgnc_records(args.hgnc_dataset)
|
|
|
|
hgnc_symbol_checker.print_symbol_map()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|