|
| 1 | +from collections.abc import Sequence |
| 2 | +from typing import Literal |
| 3 | + |
| 4 | +import pandas as pd |
| 5 | +from bioservices.mygeneinfo import MyGeneInfo |
| 6 | + |
| 7 | +__all__ = [ |
| 8 | + "convert", |
| 9 | + "determine_gene_type", |
| 10 | +] |
| 11 | + |
| 12 | +T_MG_SCOPE = Literal["entrezgene", "ensembl.gene", "symbol"] |
| 13 | +T_MG_TRANSLATE = Literal["entrez_gene_id", "ensembl_gene_id", "gene_symbol"] |
| 14 | +T_MG_RETURN = list[dict[T_MG_TRANSLATE, str]] |
| 15 | + |
| 16 | + |
| 17 | +def _get_conversion(info: MyGeneInfo, values: list[str], scope: T_MG_SCOPE, fields: str, taxon: str) -> T_MG_RETURN: |
| 18 | + value_str = ",".join(map(str, values)) |
| 19 | + results = info.get_queries(query=value_str, dotfield=True, scopes=scope, fields=fields, species=taxon) |
| 20 | + if not isinstance(results, list): |
| 21 | + raise TypeError(f"Expected results to be a list, but got {type(results)}") |
| 22 | + if not isinstance(results[0], dict): |
| 23 | + raise TypeError(f"Expected each result to be a dict, but got {type(results[0])}") |
| 24 | + |
| 25 | + data: T_MG_RETURN = [] |
| 26 | + for result in results: |
| 27 | + ensembl = result.get("query" if scope == "ensembl.gene" else "ensembl.gene") |
| 28 | + entrez = result.get("query" if scope == "entrezgene" else "entrezgene") |
| 29 | + symbol = result.get("query" if scope == "symbol" else "symbol") |
| 30 | + data.append({"ensembl_gene_id": ensembl, "entrez_gene_id": entrez, "gene_symbol": symbol}) |
| 31 | + return data |
| 32 | + |
| 33 | + |
| 34 | +def convert(ids: int | str | Sequence[int] | Sequence[str] | Sequence[int | str], taxon: int | str, cache: bool = True): |
| 35 | + """Convert between genomic identifiers. |
| 36 | +
|
| 37 | + This function will convert between the following components: |
| 38 | + - Entrez Gene ID |
| 39 | + - Ensembl Gene ID |
| 40 | + - Gene Symbol |
| 41 | +
|
| 42 | + :param ids: IDs to be converted |
| 43 | + :param taxon: Taxonomic identifier |
| 44 | + :param: scope: The type of identifier provided in `ids` |
| 45 | + :param cache: Should local caching be used for queries |
| 46 | + :return: DataFrame with columns "entrez_gene_id", "ensembl_gene_id", and "gene_symbol" |
| 47 | + """ |
| 48 | + my_geneinfo = MyGeneInfo(cache=cache) |
| 49 | + chunk_size = 1000 |
| 50 | + id_list = list(map(str, [ids] if isinstance(ids, (int, str)) else ids)) |
| 51 | + chunks = list(range(0, len(id_list), chunk_size)) |
| 52 | + |
| 53 | + data_type = determine_gene_type(id_list) |
| 54 | + if not all(v == data_type[id_list[0]] for v in data_type.values()): |
| 55 | + raise ValueError("All items in ids must be of the same type (Entrez, Ensembl, or symbols).") |
| 56 | + |
| 57 | + scope = next(iter(data_type.values())) |
| 58 | + fields = ",".join({"ensembl.gene", "entrezgene", "symbol"} - {scope}) |
| 59 | + taxon_str = str(taxon) |
| 60 | + return pd.DataFrame( |
| 61 | + [ |
| 62 | + row |
| 63 | + for i in chunks |
| 64 | + for row in _get_conversion( |
| 65 | + info=my_geneinfo, |
| 66 | + values=id_list[i : i + chunk_size], |
| 67 | + scope=scope, |
| 68 | + fields=fields, |
| 69 | + taxon=taxon_str, |
| 70 | + ) |
| 71 | + ] |
| 72 | + ) |
| 73 | + |
| 74 | + |
| 75 | +def determine_gene_type(items: str | list[str], /) -> dict[str, T_MG_SCOPE]: |
| 76 | + """Determine the genomic data type. |
| 77 | +
|
| 78 | + :param items: A string or list of strings representing gene identifiers. |
| 79 | + The function will determine whether each identifier is an Entrez Gene ID, |
| 80 | + Ensembl Gene ID, or a gene symbol based on its format. |
| 81 | +
|
| 82 | + :return: A dictionary mapping each input item to its determined type, which can be one of: |
| 83 | + - "entrez_gene_id": If the item consists solely of digits. |
| 84 | + - "ensembl_gene_id": If the item starts with "ENS" and is |
| 85 | + followed by a specific format (length greater than 11 and the last 11 characters are digits). |
| 86 | + - "gene_symbol": If the item does not match the above criteria, it is assumed to be a gene symbol. |
| 87 | + """ |
| 88 | + items = [items] if isinstance(items, str) else items |
| 89 | + |
| 90 | + determine: dict[str, Literal["entrezgene", "ensembl.gene", "symbol"]] = {} |
| 91 | + for i in items: |
| 92 | + i_str = str(i).split(".")[0] if isinstance(i, float) else str(i) |
| 93 | + if i_str.isdigit(): |
| 94 | + determine[i_str] = "entrezgene" |
| 95 | + elif i_str.startswith("ENS") and (len(i_str) > 11 and all(i.isdigit() for i in i_str[-11:])): |
| 96 | + determine[i_str] = "ensembl.gene" |
| 97 | + else: |
| 98 | + determine[i_str] = "symbol" |
| 99 | + |
| 100 | + return determine |
0 commit comments