From e60266427032776fba481be1a4aa5f195bbe58ba Mon Sep 17 00:00:00 2001 From: bprize15 Date: Tue, 30 Jun 2026 10:21:36 -0400 Subject: [PATCH] add back old mapping tools --- scripts/oncotree_to_oncotree.py | 592 ++++++++++++ .../README.md | 29 + .../file_comparison/README.md | 7 + .../file_comparison/file_comparison.py | 29 + .../ontology_mappings.txt | 853 ++++++++++++++++++ .../ontology_to_ontology_mapping_tool.py | 163 ++++ scripts/test/__init__.py | 0 scripts/test/test_oncotree_to_oncotree.py | 265 ++++++ 8 files changed, 1938 insertions(+) create mode 100644 scripts/oncotree_to_oncotree.py create mode 100644 scripts/ontology_to_ontology_mapping_tool/README.md create mode 100644 scripts/ontology_to_ontology_mapping_tool/file_comparison/README.md create mode 100644 scripts/ontology_to_ontology_mapping_tool/file_comparison/file_comparison.py create mode 100644 scripts/ontology_to_ontology_mapping_tool/ontology_mappings.txt create mode 100644 scripts/ontology_to_ontology_mapping_tool/ontology_to_ontology_mapping_tool.py create mode 100644 scripts/test/__init__.py create mode 100644 scripts/test/test_oncotree_to_oncotree.py diff --git a/scripts/oncotree_to_oncotree.py b/scripts/oncotree_to_oncotree.py new file mode 100644 index 00000000..d7250397 --- /dev/null +++ b/scripts/oncotree_to_oncotree.py @@ -0,0 +1,592 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2019 Memorial Sloan-Kettering Cancer Center. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF +# MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. The software and +# documentation provided hereunder is on an "as is" basis, and +# Memorial Sloan-Kettering Cancer Center +# has no obligations to provide maintenance, support, +# updates, enhancements or modifications. In no event shall +# Memorial Sloan-Kettering Cancer Center +# be liable to any party for direct, indirect, special, +# incidental or consequential damages, including lost profits, arising +# out of the use of this software and its documentation, even if +# Memorial Sloan-Kettering Cancer Center +# has been advised of the possibility of such damage. + +import argparse +import os +import sys +import urllib.request +import json + +ONCOTREE_WEBSITE_URL = "http://oncotree.mskcc.org/#/home?version=" +ONCOTREE_API_URL_BASE_DEFAULT = "http://oncotree.mskcc.org/api/" +ONCOTREE_VERSION_ENDPOINT = "versions" +ONCOTREE_TUMORTYPES_ENDPOINT = "tumorTypes" +VERSION_API_IDENTIFIER_FIELD = "api_identifier" +VERSION_RELEASE_DATE_FIELD = "release_date" +METADATA_HEADER_PREFIX = "#" +PASSTHROUGH_ONCOTREE_CODE_LIST = ["NA"] # These codes will be passed through the converter without examination or alteration +TOOL_VERSION_NUMBER = "1.2" + +# field names used for navigating tumor types +CHILDREN_CODES_FIELD = "children" +HISTORY_FIELD = "history" +ONCOTREE_CODE_FIELD = "code" +PARENT_CODE_FIELD = "parent" +PRECURSORS_FIELD = "precursors" +REVOCATIONS_FIELD = "revocations" + +# logging fields +GLOBAL_LOG_MAP = {} +CLOSEST_COMMON_PARENT_FIELD = "closest_common_parent" +CHOICES_FIELD = "choices" +NEIGHBORS_FIELD = "neighbors" +IS_LOGGED_FLAG = "logged" + +#-------------------------------------------------------------- +def fetch_oncotree_versions(oncotree_api_url_base): + # fetch available onctree versions from api + oncotree_version_endpoint_url = oncotree_api_url_base + ONCOTREE_VERSION_ENDPOINT + response = urllib.request.urlopen(oncotree_version_endpoint_url) + if response.getcode() != 200: + sys.stderr.write("ERROR (HttpStatusCode %d): Unable to retrieve OncoTree versions.\n" % (response.getcode())) + sys.exit(1) + return json.loads(response.read().decode("utf-8")) + +#-------------------------------------------------------------- +def validate_input_oncotree_versions(oncotree_versions_list, source_version, target_version): + valid_version_identifiers = [version[VERSION_API_IDENTIFIER_FIELD] for version in oncotree_versions_list] + if not source_version in valid_version_identifiers: + sys.stderr.write("ERROR: Source version (%s) is not a valid OncoTree version\n" % (source_version)) + sys.exit(1) + if not target_version in valid_version_identifiers: + sys.stderr.write("ERROR: Source version (%s) is not a valid OncoTree version\n" % (target_version)) + sys.exit(1) + +#-------------------------------------------------------------- +def validate_and_fetch_oncotree_version_release_dates(source_version, target_version, oncotree_api_url_base): + if source_version == target_version: + sys.stderr.write("Error: Source OncoTree version (%s) and target OncoTree version (%s) are the same. There is no need to convert this file.\n" % (source_version, target_version)) + oncotree_versions_list = fetch_oncotree_versions(oncotree_api_url_base) + + # validate source and target versions + validate_input_oncotree_versions(oncotree_versions_list, source_version, target_version) + + # return release dates of source and target versions from available OncoTree versions + source_oncotree_version_release_date = -1 + target_oncotree_version_release_date = -1 + for version in oncotree_versions_list: + if version[VERSION_API_IDENTIFIER_FIELD] == source_version: + source_oncotree_version_release_date = version[VERSION_RELEASE_DATE_FIELD] + if version[VERSION_API_IDENTIFIER_FIELD] == target_version: + target_oncotree_version_release_date = version[VERSION_RELEASE_DATE_FIELD] + return source_oncotree_version_release_date, target_oncotree_version_release_date + +#-------------------------------------------------------------- +def load_oncotree_version(oncotree_version_name, oncotree_api_url_base): + oncotree_nodes = {} + oncotree_tumortypes_endpoint = oncotree_api_url_base + ONCOTREE_TUMORTYPES_ENDPOINT + "?version=" + oncotree_version_name + response = urllib.request.urlopen(oncotree_tumortypes_endpoint) + if response.getcode() != 200: + sys.stderr.write("ERROR (HttpStatusCode %d): Unable to retrieve OncoTree version %s.\n" % (response.getcode(), oncotree_version_name)) + sys.exit(1) + for json_oncotree_node in json.loads(response.read().decode("utf-8")): + new_node = {} + new_node[PARENT_CODE_FIELD] = json_oncotree_node[PARENT_CODE_FIELD] + new_node[PRECURSORS_FIELD] = json_oncotree_node[PRECURSORS_FIELD] + new_node[REVOCATIONS_FIELD] = json_oncotree_node[REVOCATIONS_FIELD] + new_node[HISTORY_FIELD] = json_oncotree_node[HISTORY_FIELD] + new_node[ONCOTREE_CODE_FIELD] = json_oncotree_node[ONCOTREE_CODE_FIELD] + new_node[CHILDREN_CODES_FIELD] = [] + oncotree_nodes[json_oncotree_node[ONCOTREE_CODE_FIELD]] = new_node + # second pass, add in children + for oncotree_node in oncotree_nodes.values(): + try: + oncotree_nodes[oncotree_node[PARENT_CODE_FIELD]][CHILDREN_CODES_FIELD].append(oncotree_node[ONCOTREE_CODE_FIELD]) + except: + continue + return oncotree_nodes + +#-------------------------------------------------------------- +def get_header(file): + header = [] + with open(file, "r") as header_source: + for line in header_source: + if not line.startswith("#"): + header = line.rstrip().split('\t') + break + return header +#-------------------------------------------------------------- +# Takes a data_clinical_sample.txt file +# Saves header/commented lines as strings {row number: row} +# Additional processing to add ONCOTREE_CODE_OPTIONS column +def load_source_file(source_file): + header = get_header(source_file) + header_length = len(header) + headers_processed = False + source_file_mapped_list = [] + header_and_comment_lines = {} + header_line_number = 0 + + if "ONCOTREE_CODE" not in header: + sys.stderr.write("ERROR: Input file is missing column 'ONCOTREE_CODE'.\n") + sys.exit(1) + + with open(source_file, "r") as data_file: + for line_number, line in enumerate(data_file): + if '\r' in line: + sys.stderr.write("ERROR: source file (%s) is not in the required format (tab delimited, newline line breaks). carriage return characters encountered.\n") + sys.exit(1) + if line.startswith(METADATA_HEADER_PREFIX) or len(line.rstrip()) == 0: + header_and_comment_lines[line_number] = line + continue + if not headers_processed: + headers_processed = True + header_line_number = line_number + header_and_comment_lines[line_number] = line + continue + if len(line.split('\t')) != header_length: + sys.stderr.write("ERROR: Current row has a different number of columns than header row: %s\n" % line) + sys.exit(1) + data = dict(zip(header, map(str.strip, line.split('\t')))) + source_file_mapped_list.append(data) + + # This column has to be added after since zip was functioning on index + new_oncotree_code_index = header.index("ONCOTREE_CODE") + 1 + header.insert(new_oncotree_code_index, "ONCOTREE_CODE_OPTIONS") + + # add new column (ONCOTREE_CODE_OPTIONS) + for line_number in range(header_line_number): + header_and_comment_lines[line_number] = add_new_column(header_and_comment_lines[line_number], new_oncotree_code_index, "") + header_and_comment_lines[header_line_number] = add_new_column(header_and_comment_lines[header_line_number], new_oncotree_code_index, "ONCOTREE_CODE_OPTIONS") + # TODO: do the same thing for commented out lines inserted in random points throughout file (will have to make same changes for writing out records) + return source_file_mapped_list, header, header_and_comment_lines + +#-------------------------------------------------------------- +def add_new_column(row, new_column_index, column_name): + updated_row = row.split('\t') + # row is just a sentence (not a tab-delimited string), skip adding + if len(updated_row) < (new_column_index - 1): + return row + column_added_at_end = len(updated_row) == new_column_index + # column is being added at the end, add a return character and remove from previous last column + if column_added_at_end: + updated_row[new_column_index -1] = updated_row[new_column_index - 1].rstrip() + updated_row.insert(new_column_index, column_name + "\n") + else: + updated_row.insert(new_column_index, column_name) + return '\t'.join(updated_row) + +#-------------------------------------------------------------- +def remove_new_column(row, new_column_index): + updated_row = row.split('\t') + if len(updated_row) < new_column_index: + return row + column_removed_at_end = (len(updated_row) - 1) == new_column_index + # column was added at the end, remove new column and add return character back + if column_removed_at_end: + updated_row[new_column_index - 1] = updated_row[new_column_index - 1] + "\n" + del updated_row[new_column_index] + else: + del updated_row[new_column_index] + return '\t'.join(updated_row) + +#-------------------------------------------------------------- +# Uses a list of dictionaries, each dictionary represents a record/row +# Attempts to translate "ONCOTREE_CODE" value to target version equivalent +# Codes which map successfully (direct mapping w/o ambiguity or possible children) are placed in ONCOTREE_CODE column (ONCOTREE_CODE_OPTIONS empty) +# Codes which map ambiguously (no/possible mappings and/or new children) are placed in ONCOTREE_CODE_OPTIONS (ONCOTREE_CODE empty) +def translate_oncotree_codes(source_file_mapped_list, source_oncotree, target_oncotree, is_backwards_mapping): + for record in source_file_mapped_list: + source_oncotree_code = record["ONCOTREE_CODE"] + # initialize summary log for OncoTree code + if source_oncotree_code not in GLOBAL_LOG_MAP: + GLOBAL_LOG_MAP[source_oncotree_code] = { + NEIGHBORS_FIELD : [], + CHOICES_FIELD : [], + CLOSEST_COMMON_PARENT_FIELD : "", + IS_LOGGED_FLAG : False + } + translated_oncotree_code, is_easily_resolved = get_oncotree_code_options(source_oncotree_code, source_oncotree, target_oncotree, is_backwards_mapping) + if is_easily_resolved: + record["ONCOTREE_CODE"] = translated_oncotree_code + record["ONCOTREE_CODE_OPTIONS"] = "" + else: + record["ONCOTREE_CODE"] = "" + record["ONCOTREE_CODE_OPTIONS"] = translated_oncotree_code + return source_file_mapped_list + +#-------------------------------------------------------------- +# Given a "source" OncoTree code, return a string which can be in the following: +# 1) single code (single mapping, no children), True +# 2) single code but with children (single mapping, new children), False +# 3) multiple directly mapped options (w/ or w/o children), False +# 4) multiple related options (closest parents/children, don't include children), False +def get_oncotree_code_options(source_oncotree_code, source_oncotree, target_oncotree, is_backwards_mapping): + if source_oncotree_code in PASSTHROUGH_ONCOTREE_CODE_LIST: + return source_oncotree_code, True + if source_oncotree_code not in source_oncotree: + GLOBAL_LOG_MAP[source_oncotree_code][CHOICES_FIELD] = ["???"] + GLOBAL_LOG_MAP[source_oncotree_code][IS_LOGGED_FLAG] = True + if len(source_oncotree_code) == 0: + return "ONCOTREE_CODE column blank : use a valid OncoTree code or \"NA\"", False + else: + return ("%s -> ???, OncoTree code not in source OncoTree version" % (source_oncotree_code)), False + source_oncotree_node = source_oncotree[source_oncotree_code] + # get a set of possible codes that source code has been directly mapped to + possible_target_oncotree_codes = get_possible_target_oncotree_codes(source_oncotree_node, target_oncotree, is_backwards_mapping) + # resolve set of codes (cannot use possible_target_oncotree_nodes anymore) + target_oncotree_code, is_easily_resolved = resolve_possible_target_oncotree_codes(source_oncotree_code, possible_target_oncotree_codes, source_oncotree, target_oncotree, is_backwards_mapping) + return target_oncotree_code, is_easily_resolved + +#-------------------------------------------------------------- +# Given a "source" OncoTree code, return a set of directly mappable "target" OncoTree codes (though history, precursors, revocations) +# Rules for determining set diff based on mapping direction +def get_possible_target_oncotree_codes(source_oncotree_node, target_oncotree, is_backwards_mapping): + possible_target_oncotree_codes = set() + source_oncotree_code = source_oncotree_node[ONCOTREE_CODE_FIELD] + if is_backwards_mapping: + # Backwards mapping + # codes in history is in the target version (Same URI - different name) + possible_target_oncotree_codes.update(get_past_oncotree_codes_for_related_codes(source_oncotree_node, target_oncotree, HISTORY_FIELD)) + if not possible_target_oncotree_codes: # history overrides current code when history is present (e.g PTCLNOS) + if source_oncotree_code in target_oncotree: + possible_target_oncotree_codes.add(source_oncotree_code) + # codes in precusors is in the target version + possible_target_oncotree_codes.update(get_past_oncotree_codes_for_related_codes(source_oncotree_node, target_oncotree, PRECURSORS_FIELD)) + # skip checking codes in revocations - invalid codes which should not be considered + return possible_target_oncotree_codes + else: + # Forwards mapping + # codes where source code is in history (this should at most be 1 node - because its the same URI) + future_codes = get_future_related_oncotree_codes_for_source_code(source_oncotree_code, target_oncotree, HISTORY_FIELD) + if len(future_codes) > 1: + sys.stderr.write("ERROR: Future OncoTree has multiple codes with code %s in history\n" % (source_oncotree_code)) + sys.exit(1) + if len(future_codes) == 1: + possible_target_oncotree_codes.update(future_codes) + return possible_target_oncotree_codes + # codes where source code is in precursor (this can be more than 1, but can not intersect with revocations) + possible_target_oncotree_codes.update(get_future_related_oncotree_codes_for_source_code(source_oncotree_code, target_oncotree, PRECURSORS_FIELD)) + if len(possible_target_oncotree_codes) > 0: + return possible_target_oncotree_codes + # codes where source code is in revocations (this can be more than 1) + possible_target_oncotree_codes.update(get_future_related_oncotree_codes_for_source_code(source_oncotree_code, target_oncotree, REVOCATIONS_FIELD)) + if len(possible_target_oncotree_codes) > 0: + return possible_target_oncotree_codes + # at this point, no matches - check if source code exists in future OncoTree + if source_oncotree_code in target_oncotree: + possible_target_oncotree_codes.add(source_oncotree_code) + return possible_target_oncotree_codes + +#-------------------------------------------------------------- +# exclusively for mapping in backward direction +# looking through 'related nodes (history, precursors)', return if found in target (past) OncoTree +# i.e SLLCLL (SLL precusor, CLL precursor) -> SLL, CLL +def get_past_oncotree_codes_for_related_codes(source_oncotree_node, target_oncotree, field): + return [past_oncotree_code for past_oncotree_code in source_oncotree_node[field] if past_oncotree_code in target_oncotree] + +#-------------------------------------------------------------- +# exclusively for mapping in forward direction +# returns codes where source code is related in target (future) OncoTree +# i.e ALL -> BLL (ALL revocation), TLL (ALL revocation) +def get_future_related_oncotree_codes_for_source_code(source_oncotree_code, target_oncotree, field): + return [future_oncotree_code for future_oncotree_code, future_oncotree_node in target_oncotree.items() if source_oncotree_code in future_oncotree_node[field]] + +#-------------------------------------------------------------- +# Given a set of OncoTree codes, return a formatted string with following info (if available): +# 1a) (== 1 choice) directly mapped target OncoTree code +# 1b) (>1 choices) possible choices +# 1c) (0 choices) neighborhood/related codes +# 2) whether or not there are children +# +# * Presence of children not checked for cases with 0 choices +# since returned "options" are already in the general neighborhood +# * Additionally, if children are available, log the closest common parent for summary report +def resolve_possible_target_oncotree_codes(source_oncotree_code, possible_target_oncotree_codes, source_oncotree, target_oncotree, is_backwards_mapping): + number_of_new_children = 0 + # skip calculating number of children for case with no direct mappings + if len(possible_target_oncotree_codes) != 0: + number_of_new_children = get_number_of_new_children(source_oncotree_code, possible_target_oncotree_codes, source_oncotree, target_oncotree) + # is easily resolved if only one option with no children + is_easily_resolved = (len(possible_target_oncotree_codes) == 1 and not number_of_new_children) + + if is_easily_resolved: + oncotree_code_options = possible_target_oncotree_codes.pop() + if source_oncotree_code != oncotree_code_options and not GLOBAL_LOG_MAP[source_oncotree_code][IS_LOGGED_FLAG]: + GLOBAL_LOG_MAP[source_oncotree_code][CHOICES_FIELD].append(oncotree_code_options) + GLOBAL_LOG_MAP[source_oncotree_code][IS_LOGGED_FLAG] = True + # log if OncoTree code has changed + return oncotree_code_options, is_easily_resolved + + # case with 0 direct mappings, return neighborhood + log common parent + if len(possible_target_oncotree_codes) == 0: + neighboring_target_oncotree_codes = get_neighboring_target_oncotree_codes([source_oncotree_code], source_oncotree, target_oncotree, True, is_backwards_mapping) + oncotree_code_options = format_oncotree_code_options(source_oncotree_code, "Neighborhood: " + ','.join(neighboring_target_oncotree_codes), number_of_new_children) + closest_common_parent = get_closest_common_parent(neighboring_target_oncotree_codes, target_oncotree) + if not GLOBAL_LOG_MAP[source_oncotree_code][IS_LOGGED_FLAG]: + GLOBAL_LOG_MAP[source_oncotree_code][NEIGHBORS_FIELD].extend(neighboring_target_oncotree_codes) + GLOBAL_LOG_MAP[source_oncotree_code][CLOSEST_COMMON_PARENT_FIELD] = closest_common_parent + else: + ordered_possible_target_oncotree_codes = possible_target_oncotree_codes + if type(possible_target_oncotree_codes) is set: + ordered_possible_target_oncotree_codes = sorted(list(possible_target_oncotree_codes)) + oncotree_code_options = format_oncotree_code_options(source_oncotree_code, "{%s}" % ','.join(ordered_possible_target_oncotree_codes), number_of_new_children) + if not GLOBAL_LOG_MAP[source_oncotree_code][IS_LOGGED_FLAG]: + GLOBAL_LOG_MAP[source_oncotree_code][CHOICES_FIELD].extend(possible_target_oncotree_codes) + if number_of_new_children: + closest_common_parent = get_closest_common_parent(possible_target_oncotree_codes, target_oncotree) + GLOBAL_LOG_MAP[source_oncotree_code][CLOSEST_COMMON_PARENT_FIELD] = closest_common_parent + GLOBAL_LOG_MAP[source_oncotree_code][IS_LOGGED_FLAG] = True + return oncotree_code_options, is_easily_resolved + +#-------------------------------------------------------------- +def format_oncotree_code_options(source_oncotree_code, oncotree_code_options, number_of_new_children): + to_return = "%s -> %s" % (source_oncotree_code, oncotree_code_options) + if number_of_new_children: + to_return = to_return + ", more granular choices introduced" + return to_return + +#-------------------------------------------------------------- +# returns codes (in target version) which succesfully mapped from relatives +# will return self is source code is in target version +def get_neighboring_target_oncotree_codes(source_oncotree_codes, source_oncotree, target_oncotree, include_children, is_backwards_mapping): + immediate_relatives = set() + immediate_relatives_in_target_oncotree = set() + # collect codes which are directly related (parents + children) in first iteration, just parents in second+ iteration + for source_oncotree_code in source_oncotree_codes: + if source_oncotree[source_oncotree_code][PARENT_CODE_FIELD]: + immediate_relatives.add(source_oncotree[source_oncotree_code][PARENT_CODE_FIELD]) + if source_oncotree[source_oncotree_code][CHILDREN_CODES_FIELD] and include_children: + immediate_relatives.update(source_oncotree[source_oncotree_code][CHILDREN_CODES_FIELD]) + + for source_oncotree_code in source_oncotree_codes: + immediate_relatives_in_target_oncotree.update(get_possible_target_oncotree_codes(source_oncotree[source_oncotree_code], target_oncotree, is_backwards_mapping)) + + # no immediate relative could be mapped backwards - try again with expanded search + if not immediate_relatives_in_target_oncotree: + return get_neighboring_target_oncotree_codes(immediate_relatives, source_oncotree, target_oncotree, False, is_backwards_mapping) + else: # at least one code was mapped successfully backwards + return immediate_relatives_in_target_oncotree + +#-------------------------------------------------------------- +# Returns number of new children (number of target children codes not in list of source children codes) +def get_number_of_new_children(source_oncotree_code, possible_target_oncotree_codes, source_oncotree, target_oncotree): + children_in_source = [] + children_in_target = [] + children_in_source = set(get_children([source_oncotree_code], children_in_source, source_oncotree)) + children_in_target = set(get_children(possible_target_oncotree_codes, children_in_target, target_oncotree)) + number_of_new_children = len(children_in_target - children_in_source) + return number_of_new_children + +#-------------------------------------------------------------- +# Recusively builds of all children codes under a set of given onctoree codes +def get_children(oncotree_codes, all_children_codes, target_oncotree): + children = [] + for oncotree_code in oncotree_codes: + children.extend(target_oncotree[oncotree_code][CHILDREN_CODES_FIELD]) + if children: + all_children_codes.extend(children) + return get_children(children, all_children_codes, target_oncotree) + else: + return all_children_codes + +#-------------------------------------------------------------- +# get the common parent (furthest down the tree) for a set of oncotree_codes +def get_closest_common_parent(possible_target_oncotree_codes, target_oncotree): + oncotree_code_to_ancestors_mapping = {} + # for every possible target OncoTree code - construct an ordered list of ancestors + for oncotree_code in possible_target_oncotree_codes: + oncotree_code_ancestors = [oncotree_code] + oncotree_code_to_ancestors_mapping[oncotree_code] = get_ancestors(oncotree_code, oncotree_code_ancestors, target_oncotree) + min_length = min([len(ancestor_list) for ancestor_list in oncotree_code_to_ancestors_mapping.values()]) + # look across lists to find the earliest point where codes differ + closest_common_parent = get_earliest_common_parent(min_length, list(oncotree_code_to_ancestors_mapping.values())) + return closest_common_parent + +#-------------------------------------------------------------- +# used to construct list of ancestors - if parent exists, insert at beginning, and call again at a higher level +def get_ancestors(oncotree_code, oncotree_code_ancestors, target_oncotree): + if target_oncotree[oncotree_code][PARENT_CODE_FIELD]: + parent_oncotree_code = target_oncotree[oncotree_code][PARENT_CODE_FIELD] + oncotree_code_ancestors.insert(0, parent_oncotree_code) + return get_ancestors(parent_oncotree_code, oncotree_code_ancestors, target_oncotree) + else: + return oncotree_code_ancestors + +#-------------------------------------------------------------- +def get_earliest_common_parent(min_length, lists_of_all_ancestors): + # set reference code as the code at index (min_length - 1) for first list in lists_of_all_ancestors + oncotree_code = lists_of_all_ancestors[0][min_length - 1] + # skip first list in lists_of_all_ancestors + for list_of_ancestors in lists_of_all_ancestors[1:]: + if min_length > 0: + if list_of_ancestors[min_length - 1] != oncotree_code: + # at first mismatch, decrement min_length to move earlier in the list + min_length -= 1 + return get_earliest_common_parent(min_length, lists_of_all_ancestors) + return oncotree_code + +#-------------------------------------------------------------- +def write_to_target_file(translated_source_file_mapped_list, target_file, header, header_and_comment_lines): + all_easily_resolved = True + oncotree_code_options_index = header.index("ONCOTREE_CODE_OPTIONS") + for record in translated_source_file_mapped_list: + if record["ONCOTREE_CODE_OPTIONS"]: + all_easily_resolved = False + break + if all_easily_resolved: + header.remove("ONCOTREE_CODE_OPTIONS") + for line_number in range(len(header_and_comment_lines)): + header_and_comment_lines[line_number] = remove_new_column(header_and_comment_lines[line_number], oncotree_code_options_index) + + line_num = 0 + with open(target_file, "w") as f: + for record in translated_source_file_mapped_list: + while line_num in header_and_comment_lines: + f.write(header_and_comment_lines[line_num]) + line_num += 1 + formatted_data = map(lambda x: record.get(x,''), header) + f.write('\t'.join(formatted_data) + '\n') + line_num += 1 + sys.stderr.write("Primary target file written to %s\n" % (target_file)) + +#-------------------------------------------------------------- +# sorts logging map based on resolution type +# (e.g show unmappable nodes before ambiguous nodes +def sort_by_resolution_method(oncotree_code, logged_code): + # no direct mappings first + if logged_code[NEIGHBORS_FIELD]: + return "0" + oncotree_code + # has multiple possible choices and has children + elif len(logged_code[CHOICES_FIELD]) > 1 and logged_code[CLOSEST_COMMON_PARENT_FIELD]: + return "1" + oncotree_code + # has multiple possible choices and has no children + elif len(logged_code[CHOICES_FIELD]) > 1 and not logged_code[CLOSEST_COMMON_PARENT_FIELD]: + return "2" + oncotree_code + # has one choice and has children + elif len(logged_code[CHOICES_FIELD]) == 1 and logged_code[CLOSEST_COMMON_PARENT_FIELD]: + return "3" + oncotree_code + else: + return "4" + oncotree_code + +#-------------------------------------------------------------- +def write_summary_file(target_file, source_version, target_version): + oncotree_url = ONCOTREE_WEBSITE_URL + target_version + # Break logged nodes into subcategories and sort (alphabetically and resolution type) + # For each category, codes with more granular codes introduced are shown first + unmappable_codes = sorted([unmappable_code for unmappable_code, unmappable_node in GLOBAL_LOG_MAP.items() if unmappable_node[NEIGHBORS_FIELD]]) + ambiguous_codes = sorted([ambiguous_code for ambiguous_code, ambiguous_node in GLOBAL_LOG_MAP.items() if len(ambiguous_node[CHOICES_FIELD]) > 1], key = lambda k: sort_by_resolution_method(k, GLOBAL_LOG_MAP[k])) + partially_resolved_codes = sorted([resolved_code for resolved_code, resolved_node in GLOBAL_LOG_MAP.items() if len(resolved_node[CHOICES_FIELD]) == 1 and resolved_node[CLOSEST_COMMON_PARENT_FIELD] and ("???" not in resolved_node[CHOICES_FIELD])], key = lambda k: sort_by_resolution_method(k, GLOBAL_LOG_MAP[k])) + completely_resolved_codes = sorted([resolved_code for resolved_code, resolved_node in GLOBAL_LOG_MAP.items() if len(resolved_node[CHOICES_FIELD]) == 1 and not resolved_node[CLOSEST_COMMON_PARENT_FIELD] and ("???" not in resolved_node[CHOICES_FIELD])]) + unrecognized_codes = sorted([unrecognized_code for unrecognized_code, unrecognized_node in GLOBAL_LOG_MAP.items() if ("???" in unrecognized_node[CHOICES_FIELD]) ], key = lambda k: sort_by_resolution_method(k, GLOBAL_LOG_MAP[k])) + + html_summary_file = os.path.splitext(target_file)[0] + "_summary.html" + with open(html_summary_file, "w") as f: + # General info + f.write("\n\n\nMapping Summary\n\n\n\n") + f.write("

Mapping Summary

\n") + f.write("

Tool version: v.%s
" % (TOOL_VERSION_NUMBER)) + f.write("Mapped %s to %s
" % (source_version, target_version)) + f.write("All resolutions should be made with version: %s\n" % (oncotree_url, target_version)) + f.write("

Contents

 \n \n") + # Unrecognized codes - action required, but not guidance. Just list them + if unrecognized_codes: + f.write("

The following codes were not present in the source OncoTree version:

\n") + for oncotree_code in unrecognized_codes: + f.write("

Original Code: %s
\n" % ("<blank>" if len(oncotree_code) == 0 else oncotree_code)) + f.write("New Code cannot be determined\n") + # Unmappable codes - printed first since they MUST be resolved with manual tree exploration + if unmappable_codes: + f.write("


The following codes could not be mapped to a code in the target version and require additional resolution:

\n") + for oncotree_code in unmappable_codes: + f.write("

Original Code: %s
\n" % (oncotree_code)) + f.write("Closest Neighbors: %s
\n" % ','.join(GLOBAL_LOG_MAP[oncotree_code][NEIGHBORS_FIELD])) + f.write("To resolve, please refer to closest shared parent node %s and its descendants here

\n" % ((GLOBAL_LOG_MAP[oncotree_code][CLOSEST_COMMON_PARENT_FIELD]), (oncotree_url + "&search_term=(" + GLOBAL_LOG_MAP[oncotree_code][CLOSEST_COMMON_PARENT_FIELD] + ")"))) + # Ambiguous codes - printed second since they MUST be resolved but already provide choices + if ambiguous_codes: + f.write("

The following codes mapped to multiple codes in the target version. Please select from provided choices:

\n") + for oncotree_code in ambiguous_codes: + f.write("

Original Code: %s
\n" % (oncotree_code)) + f.write("Choices: %s
\n" % ','.join(GLOBAL_LOG_MAP[oncotree_code][CHOICES_FIELD])) + if GLOBAL_LOG_MAP[oncotree_code][CLOSEST_COMMON_PARENT_FIELD]: + f.write("*Warning: Target version has introduced more granular nodes.
\n") + f.write("You can examine the closest shared parent node %s and its descendants here

\n" % ((GLOBAL_LOG_MAP[oncotree_code][CLOSEST_COMMON_PARENT_FIELD]), (oncotree_url + "&search_term=(" + GLOBAL_LOG_MAP[oncotree_code][CLOSEST_COMMON_PARENT_FIELD] + ")"))) + # Directly mapped codes - no action required, might want to explore more granular choices + if partially_resolved_codes: + f.write("

The following codes mapped to exactly one code but more granular codes have been introduced:

\n") + for oncotree_code in partially_resolved_codes: + f.write("

Original Code: %s
\n" % (oncotree_code)) + f.write("New Code: %s
\n" % ','.join(GLOBAL_LOG_MAP[oncotree_code][CHOICES_FIELD])) + if GLOBAL_LOG_MAP[oncotree_code][CLOSEST_COMMON_PARENT_FIELD]: + f.write("*Warning: Target version has introduced more granular nodes.
\n") + f.write("You can examine the closest shared parent node %s and its descendants here

\n" % ((GLOBAL_LOG_MAP[oncotree_code][CLOSEST_COMMON_PARENT_FIELD]), (oncotree_url + "&search_term=(" + GLOBAL_LOG_MAP[oncotree_code][CLOSEST_COMMON_PARENT_FIELD] + ")"))) + # Directly mapped codes - no action required, might want to explore more granular choices + if completely_resolved_codes: + f.write("

The following codes mapped to exactly one code:

\n") + for oncotree_code in completely_resolved_codes: + f.write("

Original Code: %s
\n" % (oncotree_code)) + f.write("New Code: %s
\n" % ','.join(GLOBAL_LOG_MAP[oncotree_code][CHOICES_FIELD])) + sys.stderr.write("Mapping summary HTML file written out to %s\n" % (html_summary_file)) + +def usage(parser, message): + if message: + sys.stderr.write("%s\n" % (message)) + sys.stderr.write("%s\n" % (parser.print_help())) + sys.exit(1) + +#-------------------------------------------------------------- +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("-a", "--auto-mapping-enabled", help = "enable automatic resolution of ambiguous mappings", action = "store_true") + parser.add_argument("-i", "--source-file", help = "source file provided by user", required = True) + parser.add_argument("-o", "--target-file", help = "destination file to write out new file contents", required = True) + parser.add_argument("-s", "--source-version", help = "current OncoTree version used in the source file", required = True) + parser.add_argument("-t", "--target-version", help = "OncoTree version to be mapped to in the destination file", required = True) + parser.add_argument("-u", "--oncotree-url", required = False, help=argparse.SUPPRESS) + args = parser.parse_args() + + source_file = args.source_file + target_file = args.target_file + source_version = args.source_version + target_version = args.target_version + oncotree_url = args.oncotree_url + + oncotree_api_url_base = ONCOTREE_API_URL_BASE_DEFAULT + if oncotree_url: + oncotree_api_url_base = oncotree_url + + if not source_file or not target_file or not source_version or not target_version: + usage(parse, "Error: missing arguments") + + if not os.path.isfile(source_file): + sys.stderr.write("Error: cannot access source file (%s) : no such file\n" % (source_file)) + sys.exit(1) + + source_oncotree_version_release_date, target_oncotree_version_release_date = validate_and_fetch_oncotree_version_release_dates(source_version, target_version, oncotree_api_url_base) + is_backwards_mapping = target_oncotree_version_release_date < source_oncotree_version_release_date # determines directionality of source - target OncoTree mapping + source_file_mapped_list, header, header_and_comment_lines = load_source_file(source_file) + source_oncotree = load_oncotree_version(source_version, oncotree_api_url_base) + target_oncotree = load_oncotree_version(target_version, oncotree_api_url_base) + translated_source_file_mapped_list = translate_oncotree_codes(source_file_mapped_list, source_oncotree, target_oncotree, is_backwards_mapping) + write_to_target_file(translated_source_file_mapped_list, target_file, header, header_and_comment_lines) + write_summary_file(target_file, source_version, target_version) + sys.stderr.write("OncoTree version conversion completed.\n") + +if __name__ == '__main__': + main() diff --git a/scripts/ontology_to_ontology_mapping_tool/README.md b/scripts/ontology_to_ontology_mapping_tool/README.md new file mode 100644 index 00000000..cbee90e2 --- /dev/null +++ b/scripts/ontology_to_ontology_mapping_tool/README.md @@ -0,0 +1,29 @@ +## Ontology to Ontology Mapping Tool + +The Ontology Mapping tool was developed to facilitate the mapping between different cancer classification systems. We currently support mapping between OncoTree, ICD-O, NCIt, UMLS and HemeOnc systems. + +### Prerequisites +The Ontology Mapping tool runs on python 3 and requires `pandas` and `requests` libraries. These libraries can be installed using +``` +pip3 install pandas +pip3 install requests + ``` + +### Running the tool + +The tool can be run with the following command: +``` +python --source-file --target-file --source-code --target-code +``` + +**Options** +``` + -i | --source-file: This is the source file path. The source file must contain one of the ONCOTREE_CODE, NCIT_CODE, UMLS_CODE, ICDO_TOPOGRAPHY_CODE, ICDO_MORPHOLOGY_CODE or HEMEONC_CODE in the file header and it must contain codes corresponding to the Ontology System. + -o | --target-file: This is the path to the target file that will be generated. It will contain ontologies mapped from source code in to . + -s | --source-code: This is the source ontology code in . It must be one of the ONCOTREE_CODE, NCIT_CODE, UMLS_CODE, ICDO_TOPOGRAPHY_CODE, ICDO_MORPHOLOGY_CODE or HEMEONC_CODE. + -t | --target-code: This is the target ontology code that the script will attempt to map the source file ontology code to. It must be one of the ONCOTREE_CODE, NCIT_CODE, UMLS_CODE, ICDO_TOPOGRAPHY_CODE, ICDO_MORPHOLOGY_CODE or HEMEONC_CODE. +``` + +**Note** +- The source file should be tab delimited and should contain one of the ontology: ONCOTREE_CODE, NCIT_CODE, UMLS_CODE, ICDO_TOPOGRAPHY_CODE, ICDO_MORPHOLOGY_CODE or HEMEONC_CODE in the file header. +- We currently are allowing only one ontology to another ontology mapping. In the future, we plan to extend the tool to support mapping to multiple ontology systems. diff --git a/scripts/ontology_to_ontology_mapping_tool/file_comparison/README.md b/scripts/ontology_to_ontology_mapping_tool/file_comparison/README.md new file mode 100644 index 00000000..bf6c33e9 --- /dev/null +++ b/scripts/ontology_to_ontology_mapping_tool/file_comparison/README.md @@ -0,0 +1,7 @@ +## Ontology to Ontology Mapping Tool + +This tool helps compare existing OncoTree mapped ontologies against a reference OncoTree Codes file, and gives an output file named "missing_oncotree_codes.xlsx" that consist of a list of OncoTree codes that are missing from the inputted file. In order to use this tool, you must: +1. Download the 'refrence_oncotree_codes.xlsx' file +2. Input an excel file that consists of OncoTree codes in column 1 and other ontology codes (UMLS, NCIT, SNOMED, ICDO Topography, HEMEONC, or ICDO Morphology) in the other columns. + +This tool MUST consist of OncoTree codes in column 1 to compare the two files. The output file will only give OncoTree codes that are missing from the file you inputted. diff --git a/scripts/ontology_to_ontology_mapping_tool/file_comparison/file_comparison.py b/scripts/ontology_to_ontology_mapping_tool/file_comparison/file_comparison.py new file mode 100644 index 00000000..07e5b338 --- /dev/null +++ b/scripts/ontology_to_ontology_mapping_tool/file_comparison/file_comparison.py @@ -0,0 +1,29 @@ +import pandas as pd +import os + +file_a = pd.read_excel("reference_oncotree_codes.xlsx") +oncotree_codes_a = file_a.iloc[:, 0].dropna() + +while True: + file_b = input("Enter name of the file in an Excel format ('.xlsx')") + if '.xlsx' not in file_b: + print("You forgot to add '.xlsx'. Please try again and add the required '.xlsx'.") + elif '.xlsx' in file_b: + if os.path.exists(file_b): + file_b = pd.read_excel(file_b) + file_b = file_b.astype(str) + file_b.replace({'': pd.NA}, inplace=True) + oncotree_codes_b = file_b.iloc[:, 0].dropna() + + missing_codes = oncotree_codes_a[~oncotree_codes_a.isin(oncotree_codes_b)] + + print("The following OncoTree codes from File A are NOT found in File B:") + for code in missing_codes: + print("-", code) + + + output_df = missing_codes + output_file_path = "missing_oncotree_codes.xlsx" + output_df.to_excel(output_file_path, index=False) + else: + print("This file does not exist in your folders. Try transferring the excel file into the same folder as where this code is in") diff --git a/scripts/ontology_to_ontology_mapping_tool/ontology_mappings.txt b/scripts/ontology_to_ontology_mapping_tool/ontology_mappings.txt new file mode 100644 index 00000000..2eaf161c --- /dev/null +++ b/scripts/ontology_to_ontology_mapping_tool/ontology_mappings.txt @@ -0,0 +1,853 @@ +ONCOTREE_CODE NCIT_CODE UMLS_CODE ICDO_TOPOGRAPHY_CODE ICDO_MORPHOLOGY_CODE HEMEONC_CODE +MMB C3706 C0205833 +AIS C4123 C0334276 C80.9 8140/2 +AASTR C9477 C0334579 C72.9 9401/3 +FL C3209 C0024301 C42.4 9690/3 599 +VIMT C4286 C0334520 C57.9 9080/3 +KIDNEY C12415 C0022646 +MDEP C4327 C0334596 C72.9 9501/3 +PAOS C8969 C0206642 C41.9 9192/3 +PRSCC C6766 C1300585 C61.9 8002/3 +DSTAD C9159 C0279635 C16.9 8145/3 +SECOS C53704 C1710042 C41.9 9184/3 +ARMS C3749 C0206655 C49.9 8920/3 +PT C7575 C0238031 C50.9 9020/1 +MSCHW C6970 C1306247 C47.9 9560/1 +SCST C4862 C0600113 C56.9 8590/1 +MBC C5164 C1334708 C50.9 8575/3 +SCCE C7982 C0279674 C53.9 8002/3 +AWDNET C96422 C3272767 C18.1 8240/3 +ROCY C4526 C0346255 C64.9 8290/0 +VMM C27394 C2004576 C57.9 8746/3 +LAM C38153 C0349649 C34.9 9174/1 +CHDM C2947 C0008487 C41.9 9370/3 +ACPP C53686 C1266176 C72.9 9390/1 +PSTAD C5472 C1333785 C16.9 8260/3 +MEL C3224 C0025202 C44.9 8720/3 629 +CABC C40213 C1516403 +SCCO C27390 C2212006 C56.9 8041/3 +MFS C6496 C0334454 C49.9 8811/3 +GNBL C3790 C0206718 C47.9 9490/3 +CM C4550 C0346360 C69.0 8720/3 +PRCC C6975 C1306837 C64.9 8260/3 +BLPT C5316 C1332592 +SELT C4944 C0748616 C72.9 8000/3 +LUPC C45542 C1711397 C34.9 8022/3 +VMGCT C4290 C0334524 C57.9 9085/3 +GCLC C4452 C0345960 C34.9 8031/3 +UASC C4519 C0346202 C55.9 8560/3 +PXA C4323 C0334586 C72.9 9424/3 +EPM C3017 C0014474 C72.9 9391/3 +OFMT C6582 C1266128 C49.9 8842/0 +PACT C41247 C1518872 C25.9 8000/3 +LUSC C3493 C0149782 C34.9 8070/3 +FLC C4131 C0334287 C22.0 8171/3 +CMC C60641 C1880119 C18.9 8510/3 +SRAP C43554 C1711320 C18.1 8490/3 +ATM C4723 C0431122 C70.9 9539/1 +MASCC C40358 C1519487 +CHOM C6908 C1370510 +LUNG C12468 C0024109 C34.9 8000/3 46104 +PCNSL C9301 C0280803 C42.4 9680/3 +LXSC C4044 C0280324 C32.9 8070/3 +ADRENAL_GLAND C12666 C0001625 C74.9 8000/3 +RHM C6909 C0259786 C70.9 9538/3 +WDLS C4250 C1370889 C49.9 8851/3 +PEMESO C7633 C1377610 C48.2 9050/3 +HNSC C34447 C1168401 C76.0 8070/3 +AFX C4246 C0346053 +DESM C37257 C0334439 C44.9 8745/3 +MBOV C40036 C0279664 C56.9 8472/1 +OEC C8108 C0346183 +SCLC C4917 C0149925 C34.9 8041/3 666 +PMA C40315 C1519086 C72.9 9425/3 +PSCC C7729 C0238348 C60.9 8070/3 +ACBC C5130 C1332167 C50.9 8200/3 +CEAIS C4520 C0346203 C53.9 8140/2 +ODYS C8106 C0346185 C56.9 9060/3 +OPHSC C8181 CL497390 C10.9 8070/3 643 +PD C3301 C1704323 C50.0 8540/3 +SUBE C3795 C0206725 C72.9 9383/1 +ARMM C4639 C0349538 C21.8 8746/3 +TISSUE C12801 C0040300 +FIOS C4020 C0279602 C41.9 9182/3 +HGSOC C56.9 8441/3 +THHC C4946 C0749424 C73.9 8290/3 +LUNE C6875 C1265996 C34.9 8013/3 +THME C3879 C0238462 C73.9 8510/3 +IDC C4194 C1134719 C50.9 8521/3 +TSTAD C5473 C1333791 C16.9 8211/3 +OGBL C39985 C1518716 +PSC C5712 C1335316 C25.9 8441/0 +RSCC C116317 C2212425 C64.9 8002/3 +BLCA C39851 C0279680 C67.9 8120/3 569 +BCCA C2948 C0008497 C72.9 9100/3 +NMZL C8863 C0855139 +SWDNET C95871 C3272399 C16.9 8240/3 +USCC C6165 C1336890 C68.0 8070/3 +EMCHS C27502 C1275278 C41.9 9231/3 +SRCBC C39823 C1512742 C67.9 8490/3 +ONBL C3789 C0206717 C72.9 9522/3 +AMPCA C3908 C0262401 C24.1 8010/3 651 +CCS C3745 C0206651 C49.9 9044/3 +IMTL C39740 C1518038 C34.9 8827/1 +MFH C4247 C0334463 C49.9 8830/3 +FIBS C3043 C0016057 C49.9 8810/3 +COM C47848 C1711312 +BMGCT C4290 C0334524 C71.9 9085/3 +SCHW C3269 C0027809 C47.9 9560/0 +CERVIX C12311 C0007874 +SIC C7724 C0238196 C17.9 8010/3 +NSCLC C2926 C0007131 C34.9 8046/3 642 +PBS C4670 C0349667 C50.9 8800/3 +UMC C40144 C0854923 C55.9 8480/3 +VMT C9015 C1368910 C57.9 9080/0 +OGCT C3873 C0238324 C56.9 9064/3 +MPC C50401 C1302808 +CEVG C40208 C1516425 +ASTB C4324 C0334587 C72.9 9430/3 +MAC C7581 C0346027 +VDYS C8106 C0346185 C57.9 9060/3 +PHCH C96804 C3273047 C24.9 8160/3 +ICEMU C40203 C1516422 C53.9 8480/3 +DDCHS C6476 C0862878 C41.9 9243/3 +SACA C9272 C0948750 C08.9 8010/3 +GMN C3753 C0206660 C72.9 9064/3 +PLRMS C4258 C0334480 C49.9 8901/3 +PANCREAS C12393 C0030274 C25.9 8000/3 +OM C8562 C0558356 C69.9 8720/3 +BPSCC C6980 C1332462 C60.9 8083/3 +OSMBT C40038 C1511264 C56.9 8442/1 +RWDNET C96159 C3272610 C20.9 8240/3 +CCOC C54300 C0475829 C41.0 9341/3 +LCH C3107 C0019621 C42.4 9752/1 +LNET C5670 C1334452 C34.9 8240/3 +LUACC C5666 C1334439 C34.9 8200/3 +PCNSM C5505 C1332888 C72.9 8720/3 577 +MAAP C43558 C1706832 C18.1 8480/3 +ASPS C3750 C0206657 C49.9 9581/3 +PMHE C121668 C3840252 +PRNET C3787 C0206715 C72.9 8010/3 +STOMACH C12391 C0038351 C16.9 8000/3 38787 +BA C5184 C1332614 C50.9 9120/3 +AMPULLA_OF_VATER C13011 C0042425 +SKIN C12470 C1123023 C44.9 8000/3 46102 +BLADDER C12414 C0005682 C67.9 8000/3 +TCCA C7733 C0238449 C62.9 9100/3 +MMBL C9497 C1275668 +MUCC C3772 C0206694 C08.9 8430/3 +USTUMP C40177 C1519864 C55.9 8897/1 +USARC C6339 C0338113 C55.9 8800/3 +ECAD C28327 C1299237 C53.0 8140/3 +AMOL C4861 C0023465 C42.4 9891/3 +SOC C7550 C1335177 C56.9 8441/3 +OCS C9192 C0392998 C56.9 8980/3 +BLSC C4031 C0279681 C67.9 8070/3 +CHL C7164 C1333064 C42.4 9650/3 +MBL C3222 C0025149 C72.9 9470/3 628 +NHL C3211 C0024305 C42.4 9590/3 46089 +DSRCT C8300 C0281508 C49.9 8806/3 +ALUCA C45551 C1708766 C34.9 8249/3 +SPC C6870 C1336027 C50.9 8050/3 +CHBL C2945 C0008441 C41.9 9230/0 +EBOV C7983 C0334338 C56.9 8380/1 +THPD C6040 C1266050 C73.9 8337/3 +ES C4817 C0553580 C41.9 9260/3 597 +SGO C5932 C1335906 +UCP C5722 C1336861 C25.9 8020/3 +MRC C7572 C4049328 C64.9 8510/3 +CSCHW C4724 C0431124 C47.9 9560/0 +GBC C3844 C0235782 C23.9 8000/3 600 +DIA C9476 C0457179 +PBL C9344 C0205898 C75.3 9362/3 +EVN C92555 C2985175 C72.9 9506/1 +OVARY C12404 C0029939 C56.9 8000/3 645 +PTCA C4536 C0346300 C75.1 8272/3 +USC C27838 C0854924 C55.9 8441/3 +PERITONEUM C12770 C0031153 C48.2 8000/3 +SNA C160976 CL970005 C30.0 8140/3 +WDTC C7153 C1337013 C73.9 8010/3 +TSCST C3794 C0206724 C62.9 8590/1 +HDCS C27349 C0334663 C49.9 9757/3 +MASC C40361 C1510796 +AOAST C6959 C0431108 C72.9 9382/3 +MDS C3247 C3463824 C42.4 9989/3 634 +APAD C7718 C0238003 C18.1 8140/3 +LIVER C12392 C0023884 C22.0 8000/3 +CSCC C4819 C0553723 C44.9 8070/3 588 +EMYOCA C4199 C0334392 C76.0 8562/3 +LUAS C9133 C0279557 C34.9 8560/3 +CEMN C40254 C1516419 C53.9 9110/3 +BPDCN C7203 C1301363 C42.4 9727/3 570 +SBOV C5226 C1332598 C56.9 8442/1 +BLCLC C7266 C1332463 C34.9 8012/3 +BIMT C7014 C1332883 C71.9 9080/3 +EMBCA C6341 C0238448 C62.9 9070/3 +ADPA C27534 C1367789 +LGSOC C105556 C3642255 C56.9 8460/3 +CDRCC C6194 C1266044 C64.9 8319/3 +AITL C7528 C0020981 C42.4 9705/3 +SEBA C40310 C0206684 C44.9 8410/3 +LGCOS C6474 C1266163 C41.9 9187/3 +CHM C4871 C0678213 C55.9 9100/0 +ISTAD C9157 C0279633 C16.9 8140/3 +MCHS C53493 C1708980 C41.9 9240/3 +NPC C3871 C2931822 C11.9 8010/3 639 +LGESS C4263 C0334486 C54.1 8931/3 +SCEMU C40205 C1516424 +MBT C6974 C1334936 C71.9 8000/1 +UTUC C7716 C0220648 C68.9 8120/3 +PPCT C44.9 8000/1 +OMGCT C8114 C0280135 C56.9 9085/3 +OPE C39990 C1514199 +STSC C6764 C1333788 C16.9 8002/3 +NBL C3270 C0027819 C47.9 9500/3 640 +UA C39843 C1511204 C67.7 8140/3 +SCB C39824 C1512743 C67.9 8033/3 +CEAD C4029 C0279672 C53.9 8140/3 579 +HEMA C3085 C0018916 C49.9 9120/0 +EPIS C3714 C0205944 C49.9 8804/3 53861 +SCOAH C94537 C2986561 C75.1 8290/0 +MCHSCNS C3737 C0206637 +PLLS C3705 C0205825 C49.9 8854/3 +GCTSTM C4289 C0334523 C62.9 9084/3 +BTOV C39954 CL323981 C56.9 9000/0 +DDLS C3704 C0205824 C49.9 8858/3 +ETANTR C4915 C0700367 +BPT C5196 C1332533 +LUMEC C45544 C1708778 C34.9 8430/3 +SAAD C8021 C0279746 C08.9 8140/3 +MDLC C5160 CL007210 C50.9 8522/3 +OVT C4381 C0341823 C56.9 8010/3 +RGNT C67559 C2347979 C71.7 9509/1 +NSGCT C9313 C1336724 C62.9 9065/3 +OYST C8107 C0346188 C56.9 9071/3 +SPN C37212 C1336030 C25.9 8452/1 +WPSCC C6981 C1337009 +UAS C6336 C1336917 C55.9 8933/3 +LGFMS C45202 C1275282 C49.9 8840/3 +SDCA C5904 C1301194 C08.9 8500/3 +AGA C5609 C1266027 C21.0 8215/3 +BRSRCC C5175 C1335964 C50.9 8490/3 +TYST C8000 C0279708 C62.9 9071/3 +PAAD C8294 C0281361 C25.9 8140/3 648 +MGCT C6347 C1336720 C62.9 9085/3 +PB C4265 C0334489 C25.9 8971/3 +CECC C6344 C1332912 C53.9 8310/3 +LMS C3158 C0023269 C49.9 8890/3 54053 +SM C9235 C0221013 C42.4 9741/3 669 +SEM C9309 C0036631 C62.9 9061/3 +STAD C4004 C0278701 C16.9 8140/3 601 +DTE C27524 C0432526 +TESTIS C12412 C0039597 C62.9 8000/3 671 +POCA C5560 C1266065 C44.9 8409/3 +SGAD C3682 C1883403 C44.9 8400/3 +SCBC C9461 C1332564 C67.9 8041/3 +LCIS C4018 C0279563 C50.9 8520/2 +DIG C4738 C1321878 C72.9 8000/3 +DMBL C4956 C0751291 C72.9 9471/3 +ANSC C9161 C1412036 C21.0 8070/3 558 +UTERUS C12405 C0042149 C55.9 8000/3 +OS C9145 C0029463 C41.9 9180/3 644 +MYCF C3246 C0026948 C42.4 9700/3 586 +SARCL C45540 C1708781 C34.9 8033/3 +IMTB C6177 C1336891 C67.9 8825/1 +ODG C3288 C0751396 C72.9 9450/3 +ESS C8973 C0206630 C54.1 8930/3 +CCHM C47847 C1707042 C50.9 8575/3 +NFIB C3272 C0027830 C47.9 9540/0 +PEL C6915 C1292753 C42.4 9678/3 +MRLS C27781 C0206634 C49.9 8852/3 +GCTB C121932 C0206638 C41.9 9250/1 53894 +PINC C6966 C0917890 +GS C4221 C1266111 C49.9 8710/3 +MT C9305 C0006826 C71.9 8000/3 +GIST C3868 C0238198 C26.9 8936/1 602 +BMGT C7015 C1336704 +BRAIN C12438 C3714787 C72.9 8000/3 46090 +AML C3171 C0023467 C42.4 9840/3 552 +SPIR C4170 C0334347 C44.9 8403/0 +CCM C4722 C0431121 C72.9 9538/1 +UCA C9106 C0700101 C68.0 8000/3 +THYROID C12400 C0040132 C73.9 8000/3 675 +MPT C4504 C0346154 C50.9 9020/3 +PSTT C3757 C0206666 +LECLC C45519 C1708792 C34.9 8082/3 +PPB C5669 C1266144 C34.9 8973/3 +OSMCA C40090 C0279392 C56.9 8474/3 +BFN C40405 C1511309 +RMS C3359 C0035412 C49.9 8900/3 661 +THPA C4035 C0238463 C73.9 8260/3 +CMML C3178 C0023480 C42.4 9945/3 583 +SCOS C4023 C0279622 C41.9 9185/3 +CHRCC C4146 C1266042 C64.9 8317/3 +PNS C12465 C0206417 +AA C6936 C1306242 +ANM C4051 C0259785 C70.9 9530/3 +ATRT C6906 C1266184 C72.9 9508/3 +UEC C6287 C1336905 C55.9 8380/3 +PRNE C5545 C1335515 C61.9 8246/3 +VMA C40252 C1519925 +ASTR C60781 C0004114 C72.9 9400/3 +GB C3058 C0017636 C72.9 9440/3 +VA C7981 C0279668 C52.9 8140/3 +GNG C3788 C0206716 C72.9 9505/1 +MOV C5242 C1335168 C56.9 8480/3 +GNC C6934 CL378224 C72.9 9490/0 +APE C4049 C0280788 C72.9 9392/3 +GSARC C3796 C0206726 C72.9 9442/3 +FA C3744 C0206650 +BLAD C4032 C0279682 C67.9 8140/3 +EOV C7979 C0346163 C56.9 8380/3 +RBL C7541 C0035335 C69.2 9510/3 +STMYEC C7596 C0334699 C49.9 8982/3 +ANGL C92552 C2363903 C72.9 9431/1 +AGNG C4717 C0431112 C72.9 9505/3 +MSCC C5177 C1336079 C50.9 8070/3 +SOFT_TISSUE C12471 C0225317 C49.9 8000/3 667 +NST C4972 C0206727 C47.9 9540/3 +CCRCC C4033 C0279702 C64.9 8310/3 +CEAS C4519 C0346202 C53.9 8560/3 +UUC C6345 C0850327 C55.9 8020/3 +THYMUS C12433 C0040113 +UCEC C7558 C0476089 C54.1 8010/3 593 +CSCLC C9137 C0334240 C34.9 8045/3 +SMZL C4663 C0349632 C42.4 9689/3 +BMT C7013 C1332886 C71.9 9080/0 +UCCA C27246 C0279677 C55.9 9100/3 +MPN C4345 C1292778 C42.4 9960/3 46095 +PHM C4293 C0334529 +USTAD C5476 C1336858 C16.9 8140/3 +CPC C4715 C0431109 C72.9 9390/3 +SKCM C3510 C0151779 C44.9 8720/3 +ACC C9325 C0206686 C74.9 8370/3 555 +MLYM C3208 C0024299 C71.9 9590/3 +IDCS C9282 C1260326 C49.9 9757/3 +SRCC C27893 C1266043 C64.9 8318/3 +BL C2912 C0006413 C42.4 9687/3 573 +BRAME C6899 C1510795 C50.9 8983/0 +TRCC C27891 C1337036 C64.9 8312/3 +SCSRMS C121654 C4053999 C49.9 8912/3 +PAAC C7977 C0279661 C25.9 8550/3 +CACC C6346 C1332911 C53.9 8200/3 +PINT C3328 C1412004 C75.3 9360/1 +IHM C6985 C0008493 +SRCCR C9168,C7967 C0279654,C1707436 C18.9 8490/3 +MCL C4337 C0334634 C42.4 9673/3 625 +ACA C9003 C0206667 C74.9 8370/0 +PLEURA C12469 C0032225 +OIMT C8111 C0346182 C56.9 9080/3 +VGCE C40208 C1516425 C53.9 8262/3 +IAMPCA C27415 C1332247 C24.1 8144/3 +UELMS C40174 C1519851 C55.9 8891/3 +PRSC C5536 C1302530 C61.9 8070/3 +CESC C4028 C0279671 C53.9 8070/3 +PPTID C6967 C1367859 C75.3 9362/3 +CCOV C40076 C0346164 C56.9 8310/3 +SYNS C3400 C0039101 C49.9 9040/3 +ERMS C8971 C0206656 C49.9 8910/3 +EYE C12401 C0015392 +CUP C3812 C0220647 C80.9 8000/3 +UM C7712 C0220633 C69.4 8720/3 677 +RAML C3888 C0241961 C64.9 8860/0 +WM C80307 C0024419 C42.4 9761/3 682 +CHOL C4436 C0206698 C22.0 8160/3 580 +OTHER C17649 C0205394 C80.9 8000/3 +OCSC C4833 C0585362 C14.8 8070/3 +ANGS C3088 C0018923 C49.9 9120/3 +GEJ C9296 C1332166 C15.9 8140/3 +BOWEL C12736 C0021853 C26.0 8000/3 +CPP C3698 C0205770 C72.9 9390/0 +WT C3267 CL505178 C64.9 8960/3 683 +THYC C7569 C0205969 C37.9 8586/3 +TET C6450 C1266101 C37.9 8010/3 674 +HGNEC C96156 C3272607 C18.9 8246/3 +AODG C4326 C0334590 C72.9 9451/3 +PORO C27273 C1533161 +ILC C7950 C0279565 C50.9 8520/3 +HTAT C6846 C1266049 C73.9 8336/0 +LDD C8419 C0391826 C71.6 9493/0 +PBT C4952 C0750974 C71.9 8000/3 +ULM C3434 C0042133 C55.9 8890/0 +NMCHN C45716 C1707291 C76.0 8010/3 +UMLMS C40175 C1519861 C55.9 8896/3 +HEAD_NECK C12418 C0460004 C76.0 8000/3 608 +UDMN C36051 C1336860 C80.9 8000/3 +ACRM C4022 C0346037 C44.9 8744/3 +PHC C3326 C0031511 C74.9 8700/0 653 +DNT C9505 C1266177 C72.9 9413/0 +GRCT C3070 C0018206 C56.9 8620/1 +PENIS C12409 C0030851 650 +ACYC C2970 C0010606 C08.9 8200/3 +VPSCC C6982 C1336955 +CEEN C3769 C0206687 C53.9 8380/3 +CHS C2946 C0008479 C41.9 9220/3 +PTAD C3329 C0032000 C75.1 8272/0 +ESMM C5707 C1333460 C15.9 8746/3 +MNG C3230 C0025286 C70.9 9530/0 630 +LIPO C3194 C0023827 C49.9 8850/3 54056 +BEC C7010 C1333377 C71.9 9070/3 +CPT C3473 C0085138 C72.9 8000/3 +HPHSC C4043 C0280321 C13.9 8070/3 +LIHB C3728 C0206624 C22.0 8970/3 611 +DCIS C2924 C0007124 C50.9 8500/2 +TT C3877 C0238451 C62.9 9080/1 +MNGT C3229 C0025284 C70.9 9530/3 +ESCA C4025 C0279628 C15.9 8140/3 595 +CCBOV C40080 C0279676 +MCC C9231 C0007129 C44.9 8247/3 631 +ECD C53972 C0878675 C42.4 9750/3 594 +URCC C27892 C1336853 C64.9 8312/3 +DIPG C94764 C2986658 C72.9 9380/3 +HCL C7402 C0023443 C42.4 9940/3 607 +URCA C39842 C1511205 C67.7 8010/3 +USMT C40176 C1519863 C55.9 8897/1 +OMT C8112 C1334637 C56.9 9080/0 +ULMS C6340 C0280631 C55.9 8890/3 +MRT C3808 C0206743 C64.9 8963/3 +SEF C49027 C1710026 C49.9 8840/3 +AECA C6938 C1412016 C44.9 8401/3 +CENE C40214 C1516417 C53.9 8240/3 +LUCA C4038 C0280089 C34.9 8240/3 +TEOS C3902 C0259782 C41.9 9183/3 +MBEN C5407 C1334970 +OAST C4050 C0280793 C72.9 9382/3 +THYM C3411 C0040100 C37.9 8580/1 +DES C9182 C0079218 C49.9 8821/1 +PAASC C5721 C1335299 C25.9 8560/3 +SNSC C54287 C0334270 C30.0 8121/3 +THFO C8054 C0206682 C73.9 8331/3 +ALCL C3720 C0206180 C42.4 9714/3 560 +PECOMA C38150 C1300127 C49.9 8990/1 +BRCA C9245 C0853879 C50.9 8010/3 572 +UUS C8972 CL033042 C55.9 8930/3 +IHCH C35417 C0345905 C22.0 8160/3 +ODGC C4812 C0334558 C41.0 9270/3 +ACCC C3768 C0206685 C08.9 8550/3 +SKCN C3944 C1318558 C44.9 8761/1 +MMBC C40364 C1513365 C50.9 8575/3 +LYMPH C13252 C0024202 +LCLC C4450 C0345958 C34.9 8012/3 +STAS C5474 C1333761 C16.9 8560/3 +SCGBM C125890 C1272516 C72.9 9440/3 +COAD C4349 C0338106 C18.9 8140/3 585 +PGNT C92554 C2985174 C47.9 9509/1 +ESCC C4024 C0279626 C15.9 8070/3 595 +NLPHL C7258 C1334968 C42.4 9659/3 615 +FDCS C9281 C1260325 C42.4 9758/3 +EHAE C3800 C0206732 C49.9 9133/3 +EMBT C3264 C0027654 C72.9 9070/3 +READ C9383 C0149978 C20.9 8140/3 659 +CML C3172 C0023470 C42.4 9863/3 582 +CNC C3791 C0206719 C72.9 9506/1 +UCS C42700 C0280630 C55.9 8980/3 +IMMC C9131 C1334807 C50.9 8480/3 +MZL C4341 C1367654 C42.4 9699/3 626 +RAS C93125 C2985448 C49.9 8800/3 +SBWDNET C9461 C1332564 C17.9 8240/3 +MF C7052 C1266121 C49.9 8890/0 +MTSCC C39807 C1513719 C64.9 8032/3 +LIAS C4438 C0345907 C22.0 9124/3 +UCCC C6344 C1332912 C55.9 8310/3 +MP C3110 C0020217 +VYST C6379 C1336945 C57.9 9071/3 +HPCCNS C4660 C0349622 C72.9 9150/1 +SNUC C54294 C1710096 C30.0 8020/3 +HCCIHCH C3828 C0221287 C22.0 8180/3 +BREAST C12971 C0006141 C50.9 8000/3 44309 +EGC C9296 C1332166 C15.9 8140/3 +MYXO C6577 C0027149 C49.9 8840/0 +LIAD C3758 C0206669 C22.0 8170/0 +DF C6801 C0002991 C44.9 8832/0 +CCLC C4451 C1707407 C34.9 8310/3 +ETT C6900 C1266159 C55.9 9105/3 +IMT C6481 C0334121 C49.9 8825/1 +DDCHDM C48876 C1266174 C41.9 9372/3 +SBC C7724 C0238196 C17.9 8000/3 +LUAD C3512 C0152013 C34.9 8140/3 +MXOV C40090 C0279392 C56.9 8010/3 +SLCT C2880 C0003810 C56.9 8631/3 +INTS C53677 C1708550 C49.9 9137/3 +UAD C6167 C1336885 C68.0 8140/3 +PROSTATE C12410 C0033572 C61.9 8000/3 658 +UCU C6166 C0863015 C68.0 8120/3 +HCC C3099 C0019204 C22.0 8170/3 612 +PLMESO C9351 C1377913 C38.4 9050/3 632 +COADREAD C5105 C1319315 C18.9 8140/3 46096 +MPNST C3798 C0751690 C47.9 9540/3 +PTPR C92624 C2985219 C75.3 9395/3 +MPE C3697 C0205769 C72.9 9394/1 +CLNC C6905 C1370507 +DA C7889 C0278804 C17.0 8140/3 +PGNG C3308 C0030421 C49.9 8680/1 +HL C9357 C0019829 C42.4 9590/3 614 +MYCHS C4303 C0334551 C41.9 9231/3 +BONE C12366 C0262950 C41.9 8000/3 571 +IPMN C38342 C1518869 C25.9 8453/3 +MYEC C35700 C1335904 C08.9 8982/3 +BILIARY_TRACT C12678 C0005423 C24.9 8000/3 +CEGCC C40212 C1516407 C53.9 8015/3 +EMPD C3302 C0030186 C44.9 8542/3 +CHOS C4021 C0279603 C41.9 9181/3 +RLCLC C6876 C1265997 +CEMU C26712 C0007130 C53.9 8480/3 +PANET C27720 C1337011 C25.9 8150/1 647 +GINET C95404 C2987127 C26.9 8240/3 +SFT C7634 C1266119 C49.9 8815/0 +EPMT C6770 C1333407 C72.9 9391/3 +GCT C3474 C0085167 C72.9 9580/0 +HMBL C3801 C0206734 C72.9 9161/1 +PEOS C8970 C1377843 C41.9 9192/3 +SKAC C3775 C0206697 C44.9 8390/3 +HGSOS C53958 C1266165 C41.9 9194/3 +TLYM C6810 C0349644 C62.9 9590/3 +PNET C3716 C0206663 C72.9 9473/3 +DCS C9294 C1334030 +CCE C4714 C1384404 C72.9 9391/3 +PAST C4047 C0334583 C72.9 9421/1 +DFSP C4683 C0392784 C44.9 8832/3 +BCC C2921 C0007117 C44.9 8090/3 587 +THAP C3878 C0238461 C73.9 8021/3 +SKLMM C9151 C2739810 C44.9 8742/3 +TGCT C3401 C0039106 C49.9 9252/0 +PTCY C94524 C2986550 C75.1 9432/1 +UPECOMA C40180 C1519862 C55.9 8990/1 +SCRMS C6519 C1266134 C49.9 8912/3 +MCN C41247 C1518872 C25.9 8470/0 +GCCAP C3689 CL512511 C18.1 8243/3 +MACR C43585 C1707439 C18.9 8480/3 +AMBL C6904 C1266180 C72.9 9474/3 +IBC C4001 C0278601 C50.9 8530/3 +GTD C4699 C1135868 C55.9 9100/3 603 +CHGL C5592 C1322252 C71.9 9444/1 +RCC C9385 C0007134 C64.9 8312/3 660 +MNET C7172 C1332889 C72.9 8010/3 +VULVA C12408 C0042993 C57.9 8000/3 +SSRCC C5250 C1335965 C16.9 8490/3 +DASTR C7173 C0280785 C72.9 9400/3 +PRAD C2919 C0007112 C61.9 8140/3 +MSTAD C5248 C1334809 C16.9 8480/3 +OSOS C53953 C1704328 C41.9 9180/3 +PTES C27472 C1335563 C49.9 8804/3 +PPM C3904 C3163622 +VSC C7736 C0238518 C52.9 8070/3 680 +PMBL C9280 C1292754 C42.4 9679/3 657 +SPCC C45541 C1708784 C34.9 8801/3 +MTNN C3466 C0079772 C42.4 8000/3 46216 +HDCN C3106 C0019618 C42.4 8000/3 46099 +RDD C36075 C0019625 C42.4 9750/3 662 +MYELOID C12434 C0005767 +EMALT C3898 C0242647 C42.4 9699/3 +TLGL C4664 C1955861 C42.4 9831/3 621 +MBN C3457 CL448793 C42.4 8000/3 +CLLSLL C7540 C0855095 C42.4 9823/3 581 +PCM C3242 C0026764 C42.4 9732/3 633 +ADNOS C2852 C0001418 C80.9 8140/3 554 +AMLNPM1 C82431 C2826177 C42.4 9861/3 58170 +APLPMLRARA C9155 C0279625 C42.4 9866/3 553 +ATLL C3184 C0023493 C42.4 9827/3 556 +BLL C8936 C0023485 C42.4 9811/3 567 +BLLBCRABL1 C80331 C2698317 C42.4 9812/3 4770 +DLBCLNOS C8851 C0079744 C42.4 9680/3 589 +ENKL C4684 C0392788 598 +ET C3407 C0040028 C42.4 9962/3 596 +GBM C3058 C0017636 C72.9 9440/3 604 +GNOS C3059 C0017638 C72.9 9380/3 623 +HGGNOS C72.9 9380/3 46077 +LATL C42.4 9970/1 46091 +LBGN C42.4 8000/0 46091 +LCS C6921 C1260327 620 +LNM C7065 C0598798 C42.4 8000/3 46214 +MIDDA C2868 C0002726 622 +SS C3366 C0036920 C42.4 9701/3 586 +NETNOS C3809 C0206754 C80.9 8240/3 636 +PMF C2862 C0001815 C42.4 9961/3 +PVMF C41233 C3805232 C42.4 9950/3 +ETMF C126806 C3805233 C42.4 Unknown +PTCL C4340 C2853959 C42.4 9702/3 652 +PTLD C4727 C0432487 C42.4 9971/1 656 +PV C3336 C0032463 C42.4 9950/3 655 +SARCNOS C9118 C1261473 C49.9 8800/3 46222 +SCCNOS C2929 C0007137 C80.9 8070/3 663 +TLL C8694 C1301359 C42.4 9837/3 670 +TPLL C4752 C2363142 C42.4 9834/3 50421 +IPN C6881 C1879344 +IUP C6192 C1334282 +URMM C68.0 8746/3 +UPA C39858 C1384678 C67.9 8120/1 +ADMA C7644 C0334556 C41.9 9310/0 +LAMN C42598 C1708747 +TAC C7041 C1112503 C18.9 8211/0 +BNNOS C2910 C1458155 C50.9 8000/3 +JSCB C4189 C0334371 +DIFG C129325 CL512143 C72.9 9380/3 +ENCG C72.9 8000/3 +BGCT C5795 CL448320 C71.9 9064/3 +PCNSMT C72.9 8000/3 +CELI C128047 CL509701 +CERMS C128048 CL509707 +MCCE C53.9 8000/3 +EPDCA C95612 C2987256 C15.9 8010/3 +SMN C3751 C0206658 C16.9 8897/1 +LGT C12346 C0022907 +OHNCA C35850 C3887461 C76.0 8010/3 +HNMUCM C133187 CL520085 C76.0 8746/3 +PTH C4906 C0687150 +SBL C35837 C1335911 +CCSK C4264 C0334488 +RNET C157743 CL937404 C64.9 8000/3 +MRTL C96847 C3273076 +UESL C27096 C0855073 +LAIS C136486 CL523789 C34.9 8140/2 +MATPL C42.4 9876/3 +MBGN C42.4 8000/0 +MNM C9290 C2939461 C42.4 8000/1 +EGCT C3918 C3544268 +MIXED C80.9 8000/3 +OOVC C4908 C0677886 C56.9 8010/3 +GN C3049 C0017075 +PSEC C48.2 8461/3 +BCCP C39902 C1514507 +AN C3694 C0205748 C44.9 8727/0 +EMPSGC C167364 CL978748 C44.9 8481/3 +AFH C6494 C1266127 +IFS C4244 C0334459 +LM C3157 C0023267 +MGST C4221 C1266111 +IMS C3742 C0206648 +RCSNOS C49.9 8803/3 +TMESO C62.9 9050/3 +TNET C37.9 8240/3 +OAT C6042 C1336750 C73.9 8000/0 +OUTT C55.9 8000/1 +VGCT C128294 CL509570 +VPDC C52.9 8010/3 +PAMPCA C24.1 8163/3 +EHCH C24.9 8160/3 +GBASC C7356 C1333741 C23.9 8560/3 +GBAD C9166 C0279651 C23.9 8140/3 +SCGBC C6763 C1333759 +CCHDM C41.9 9370/3 +CTAAP C18.1 8140/3 +CAIS C18.9 8140/2 +BRCANOS C50.9 8000/3 +BRCNOS C9245 C0853879 C50.9 8010/3 +CSNOS C50.9 8980/3 +EMBC C40364 C1513365 C50.9 8575/3 +APXA C129327 CL512339 C72.9 9424/3 +LGGNOS C132067 C1997217 C72.9 9380/3 +BYST C71.9 9071/3 +SFTCNS C129526 CL512688 C72.9 8815/0 +HGNET C72.9 8010/3 +LGNET C72.9 8010/3 +MELC C9498 C0334431 +APTAD C132296 CL520582 C75.1 8140/1 +ACPG C4726 C0431129 C72.9 9351/1 +PCGP C4725 C0431128 C72.9 9352/1 +CESE C53.9 8441/3 +GRC C16.9 8140/3 +ASCT C02.9 8560/3 +HNNE C76.0 8246/3 +HNSCUP C76.0 8070/3 +PTHC C4906 C0687150 C75.0 8010/3 +HNMASC C123384 CL512709 C08.9 8502/3 +OSACA C08.9 8010/3 +NCCRCC C64.9 8010/3 +CMPT C34.9 8050/3 +NUTCL C146706 CL544717 +NSCLCPD C34.9 8046/3 +SGTTL C34.9 8200/3 +ALAL C7464 C1301357 C42.4 9801/3 +MCD C84269 C0024899 +MDS/MPN C27262 C1301355 C42.4 9975/3 +MNGLP C130038 CL514062 +MLNER C84270 C2827356 +ACN C3768 C0206685 C80.9 8550/3 +CUPNOS C80.9 8000/3 +NECNOS C3773 C0206695 C80.9 8246/3 +PDC C80.9 8010/3 +SCUP C3915 C0262584 C80.9 8042/3 +HGONEC C5238 C1335174 C56.9 8246/3 +HGSFT C57.0 8461/3 +OCNOS C4515 C0346181 +OSMAD C4508 C0346166 +SBMOV C122585 C4055371 C56.9 8460/3 +FT C3405 C0039747 C56.9 8600/0 +SCT C4215 C0334412 C56.9 8670/0 +ITPN C95506 C2987189 +PLBMESO C45665 C1709570 C38.4 9053/3 +PLEMESO C45662 C1709574 C38.4 9052/3 +PLSMESO C45663 C1709578 C38.4 9050/3 +MUP C154473 CL555395 C80.9 8720/3 +SPZM C165497 C3495721 C44.9 8772/3 +UPDC C55.9 8010/3 +UDDC C55.9 8010/3 +UMNC C55.9 9110/3 +UMEC C54.1 8010/3 +UNEC C126771 CL508215 C55.9 8246/3 +OUSARC C55.9 8800/3 +VOEC C3752 C0206659 C57.9 9070/3 +VPE C8109 +GCEMU C53.9 8480/3 +SPDAC C16.9 8010/3 +SCCRCC C27893 C1266043 C64.9 8318/3 +CCPRC C121955 C4054076 C64.9 8050/3 +FHRCC C164156 CL977349 C64.9 8312/3 +BLLRGA C80328 C2698313 C42.4 9811/3 +BLLNOS C80326 C2698310 C42.4 9811/3 +CHLPTLD C7243 C1334037 +FHPTLD C139028 CL526535 +IMPTLD C7236 C1334174 +MPTLD C7233 C1334798 +PHPTLD C7235 C1335428 +PPTLD C7183 C1301361 +ETPLL C130043 CL514044 C42.4 9837/3 +NKCLL C82217 C2826059 +AUL C9298 C1378511 C42.4 9801/3 +MPALBCRABL1 C82192 C2826037 C42.4 9806/3 +MPALKMT2A C82203 C2826048 C42.4 9807/3 +MPALBNOS C82212 C3472616 C42.4 9808/3 +MPALTNOS C82213 C2826055 C42.4 9809/3 +AMLMRC C7600 C2825139 C42.4 9895/3 +AMLRGA C7175 C1275661 C42.4 9861/3 +AMLNOS C27753 CL054841 C42.4 9861/3 +MPRDS C82338 C2826104 +MS C3520 CL414320 C42.4 9930/3 +TMN C27912 C1292776 C42.4 9920/3 +JXG C81772 C2825742 C42.4 8000/3 +FRCT C81758 C2825739 +HS C27349 C0334663 +IDCT C81767 C2825741 +CMCD C7137 C1136033 +MCSL C9348 C0036221 +MDSEB C7506 C0002894 C42.4 9983/3 +MDSID5Q C6867 C1292779 C42.4 9986/3 +MDSMD C8574 C0796466 C42.4 9985/3 +MDSRS C82616 C2826330 C42.4 9982/3 +MDSSLD C82591 C2826318 C42.4 9989/3 +MDSU C8648 C0854809 C42.4 9989/3 +RCYC C82596 C2826323 +ACML C3519 C0349640 C42.4 9876/3 +JMML C9233 C0349639 C42.4 9946/3 +MDSMPNRST C82616 C2826330 C42.4 9982/3 +MDSMPNU C27780 C1328061 C42.4 9975/3 +MLNFGFR1 C84277 C2827362 +MLNPCM1JAK2 C129853 CL512939 +MLNPDGFRA C84275 C2827360 C42.4 9965/3 +MLNPDGFRB C84276 C3472621 +CELNOS C4563 C0346421 C42.4 9964/3 +CNL C3179 C0023481 C42.4 9963/3 +MPNU C27350 C1333046 C42.4 8000/0 +BTBEOV C4746 C0474834 +BTBOV C9459 C0334494 +BTMOV C4270 C0334495 +HGESS C126998 CL508195 C54.1 8930/3 +ABC C36081 C1333296 +GCB C38335 C1517532 C42.4 9680/3 +DFL C138185 CL525720 +ISFN C138181 CL525716 +ISMCL C138191 CL525722 +MGUSIGA C565 C0020835 +MGUSIGG C568 C0020852 +MGUSIGM C569 C0020861 +MIDDO C7151 CL357425 +HCL-V C7401 C0349633 +SDRPL C80309 C2699508 +ALCLALKN C37194 C1332078 +ALCLALKP C37193 C1332079 +BIALCL C139012 CL526545 C42.4 9702/3 +LYP C3721 C0206182 +PCALCL C6860 C1301362 +BLLHYPER C80335 C2698311 C42.4 9815/3 +BLLHYPO C80338 C2698312 C42.4 9816/3 +BLLIAMP21 C130039 CL513772 +BLLETV6RUNX1 C80334 C2698314 +BLLTCF3PBX1 C80341 C2698315 C42.4 9818/3 +BLLIL3IGH C80340 C2698316 +BLLKMT2A C80332 C2698309 C42.4 9813/3 +BLLBCRABL1L C129788 CL512918 +LDCHL C9283 C0152267 +LRCHL C6913 C1266194 +MCCHL C3517 C0152266 +NSCHL C3518 C0152268 +ALKLBCL C7225 C1333294 +AHCD C3132 C0021071 +BCLU C37869 C1333878 +BPLL C4753 C0475801 +BLL11Q C131911 CL520452 +DLBCLCI C80289 C2699776 +EBVDLBCLNOS C80281 C2700007 C42.4 9680/3 +EBVMCU C131906 CL519556 +EP C4002 C0278619 +GHCD C3083 C0018854 +HHV8DLBCL C138320 CL525847 +HGBCL C80291 C2698294 +HGBCLMYCBCL2 C131913 CL524351 C42.4 9680/3 +IVBCL C4342 C0334660 +LBLIRF4 C133494 CL521195 +LYG C7930 C0024307 +LPL C3212 C0334633 +MCBCL C80310 C2698259 +MGUS C3996 C0026470 +MIDD C7151 CL357425 +MHCD C3892 C0242310 +PTFL C80297 C2698750 +PLBL C7224 C3472614 +PCLBCLLT C45194 C1709656 +PCFCL C7217 C1333171 +SPB C7812 C0272256 +SBLU C80308 C2699507 +THRLBCL C9496 C1321547 C42.4 9688/3 +ANKL C8647 C1292777 +CLPDNK C39591 C1512709 +EATL C4737 C0456889 +FTCL C80375 C2700204 +HSTCL C8459 C0522627 +HVLL C45327 C1708397 +ITLPDGI C139021 CL526526 +MEITL C96058 C3272525 +NPTLTFH C139011 CL526546 +PCATCL C139023 CL526531 +PCLPD C7195 C1371159 +PCSMTPLD C45366 CL524855 +PCAECTCL C45339 C1709653 C42.4 9709/3 +PCGDTCL C45340 C1707547 +SPTCL C6918 C0522624 +SEBVTLC C80374 C2699747 +AMLRBM15MKL1 C82427 C2826173 +AMLBCRABL1 C129785 CL512855 C42.4 9861/3 +AMLCEBPA C129782 CL512856 C42.4 9861/3 +AMLRUNX1 C129786 CL512858 +AMLRARA C36055 C2825128 +AMLCBFBMYH11 C9287 C0522630 C42.4 9871/3 +AMLGATA2MECOM C82426 C2826172 C42.4 9869/3 +AMLDEKNUP214 C82423 C2826169 C42.4 9865/3 +AMLRUNX1RUNX1T1 C9288 C1292774 C42.4 9896/3 +AMLMLLT3KMT2A C156719 CL935859 C42.4 9897/3 +AM C7961 C0279624 C42.4 9874/3 +AMLMD C8460 C0522631 C42.4 9872/3 +AWM C9380 C0279623 C42.4 9873/3 +ABL C3164 C0023437 +AMKL C3170 C0023462 C42.4 9910/3 +AMML C7463 C0023479 C42.4 9867/3 +APMF C4344 C0334674 C42.4 9931/3 +PERL C7467 CL028054 C42.4 9840/3 +MLADS C43223 C2825149 +TAM C82339 C1834582 +TAML C8252 C1336735 C42.4 9920/3 +TMDS C27722 C1292780 C42.4 9987/3 +ASM C9285 C1112486 +ISM C9286 C0272203 C42.4 9741/1 +SMMCL C3169 C0023461 +SSM C115460 CL388369 +SMAHN C9284 C1301365 C42.4 9741/3 +MDSEB1 C7167 CL514065 C42.4 9983/3 +MDSEB2 C7168 CL514066 C42.4 9983/3 +MDSRSMD C27726 C1301356 C42.4 9982/3 +MDSRSSLD C130037 CL514168 C42.4 9982/3 +CMML0 C130035 CL513815 C42.4 9945/3 +CMML1 C36061 C1333043 C42.4 9945/3 +CMML2 C36062 C1333044 C42.4 9945/3 +CMLBCRABL1 C9128 C1292771 C42.4 9876/3 +PMFPES C41237 C1516553 C42.4 9961/3 +PMFOFS C41238 C1516552 C42.4 9961/3 diff --git a/scripts/ontology_to_ontology_mapping_tool/ontology_to_ontology_mapping_tool.py b/scripts/ontology_to_ontology_mapping_tool/ontology_to_ontology_mapping_tool.py new file mode 100644 index 00000000..6badca2c --- /dev/null +++ b/scripts/ontology_to_ontology_mapping_tool/ontology_to_ontology_mapping_tool.py @@ -0,0 +1,163 @@ +# Copyright (c) 2020 Memorial Sloan-Kettering Cancer Center. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF +# MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. The software and +# documentation provided hereunder is on an "as is" basis, and +# Memorial Sloan-Kettering Cancer Center +# has no obligations to provide maintenance, support, +# updates, enhancements or modifications. In no event shall +# Memorial Sloan-Kettering Cancer Center +# be liable to any party for direct, indirect, special, +# incidental or consequential damages, including lost profits, arising +# out of the use of this software and its documentation, even if +# Memorial Sloan-Kettering Cancer Center +# has been advised of the possibility of such damage. + +import sys +import os +import io +import argparse +import pandas as pd +import requests + +ONTOLOGY_MAPPINGS_FILE_URL = "https://raw.githubusercontent.com/cBioPortal/oncotree/master/scripts/ontology_to_ontology_mapping_tool/ontology_mappings.txt" +VALID_ONCOTREE_CODES_URL = "https://raw.githubusercontent.com/cBioPortal/oncotree/master/resources/resource_uri_to_oncocode_mapping.txt" +ACCEPTED_ONTOLOGIES = ['ONCOTREE_CODE', 'UMLS_CODE', 'NCIT_CODE', 'ICDO_TOPOGRAPHY_CODE', 'HEMEONC_CODE', 'ICDO_MORPHOLOGY_CODE'] +NULL_VALUES = ['', 'NA'] + +def add_comments_column_and_log_data(mapped_data, target_file, source_code, target_code, source_file): + comments = [] + completely_resolved_codes = {} + ambiguous_codes = {} + unrecognized_codes = [] + many_to_one_codes = {} + mapped_codes_count = 0 + unmapped_codes_count = 0 + + columns_to_groupby = mapped_data.columns[:len(mapped_data.columns)-1] + grouped_data = mapped_data.groupby(list(columns_to_groupby), sort=False)[target_code].unique().apply(', '.join).reset_index() + + oncotree_codes_list = pd.read_csv(io.StringIO(requests.get(VALID_ONCOTREE_CODES_URL).content.decode('utf-8')), sep='\t', header=None, keep_default_na=False) + valid_oncotree_codes = oncotree_codes_list.loc[oncotree_codes_list[1] == "hasCode"][2].str.upper().tolist() + + for sc, tc in zip(grouped_data[source_code], grouped_data[target_code]): + if tc == "": #one_to_none mapping + if source_code == "ONCOTREE_CODE" and sc not in valid_oncotree_codes: + comments.append("Invalid code") + else: + comments.append((lambda sc: 'No mapping found' if sc not in NULL_VALUES else '')(sc)) + unrecognized_codes.append(sc) + if sc not in NULL_VALUES: + unmapped_codes_count += 1 + elif len(tc.split(', ')) > 1: #one_to_many mapping + comments.append('Maps to multiple codes') + ambiguous_codes[sc] = tc + mapped_codes_count += 1 + else: #one_to_one mapping + comments.append('') + completely_resolved_codes[sc] = tc + mapped_codes_count += 1 + + #Also print many_to_one mapping to Log file (Multiple source codes map to one target code) + many_to_one = mapped_data.groupby(target_code, sort=False)[source_code].unique().apply(', '.join).reset_index() + for tc, sc in zip(many_to_one[target_code], many_to_one[source_code]): + if tc != '' and len(sc.split(', ')) > 1: + many_to_one_codes[tc] = sc + + grouped_data['COMMENTS'] = comments + grouped_data.to_csv(target_file, sep='\t', index=False) + + log_file = open(os.path.splitext(target_file)[0] + "_summary.html", 'w') + log_file.write("\n\n\nOntology Mapping Summary\n\n\n\n") + log_file.write("

Ontology Mapping Summary

\n") + log_file.write("Source Ontology: %s
" % (source_code)) + log_file.write("Target Ontology: %s

" % (target_code)) + log_file.write("Mapped %s to %s

" % (source_code, target_code)) + log_file.write("
Total Mapped Entries %s
Total Unmapped Entries %s
" % (mapped_codes_count, unmapped_codes_count)) + + if completely_resolved_codes: + log_file.write("

The following source codes mapped to one target code:

\n") + for code in completely_resolved_codes: + log_file.write("

Source Code: %s
\n" % (code)) + log_file.write("Target Code: %s
\n" % completely_resolved_codes[code]) + if ambiguous_codes: + log_file.write("


The following source codes mapped to multiple target codes:

\n") + for code in ambiguous_codes: + log_file.write("

Source Code: %s
\n" % (code)) + log_file.write("Target Codes: %s
\n" % ambiguous_codes[code]) + if unrecognized_codes: + log_file.write("


The following source codes have no mapping available:

\n") + for code in unrecognized_codes: + log_file.write("

Source Code: %s
\n" % ("<blank>" if len(code) == 0 else code)) + log_file.write("Mapping not available for the code\n") + if many_to_one_codes: + log_file.write("


The following target codes mapped to multiple source codes:

\n") + for code in many_to_one_codes: + log_file.write("

Target Code: %s
\n" % (code)) + log_file.write("Source Codes: %s
\n" % many_to_one_codes[code]) + + log_file.close() + +#Check if any of the ontology code headers are present in the clinical file +def validate_arguments(source_file, source_code, target_code): + #1. Check if the source_code input by the user is valid. + if source_code not in ACCEPTED_ONTOLOGIES: + print("Invalid source code: \'%s\'. \nThe list of acceptable codes are:" % (source_code)) + for code in ACCEPTED_ONTOLOGIES: + print(code) + sys.exit(1) + + #2. Check if the target_code input by the user is valid. + if target_code not in ACCEPTED_ONTOLOGIES: + print("Invalid target code: \'%s\'. \nThe list of acceptable codes are:" % (target_code)) + for code in ACCEPTED_ONTOLOGIES: + print(code) + sys.exit(1) + + #3. Check if source_code and target_code are not the same + if source_code == target_code: + print("The source and target ontology columns to be mapped are the same.\nSource Ontology: %s\nTarget Ontology: %s" % (source_code, target_code)) + print("Please specify different ontologies to map on.\n") + sys.exit(1) + + #4. CHeck if the user input file has the source code column. + source_file.columns = map(str.upper, source_file.columns) + if source_code not in source_file.columns: + print("\nThe input file does not contain the requested source ontology column or the headers do not match to accepted values. Please check. \n\nThe list of acceptable ontology headers are:") + for code in ACCEPTED_ONTOLOGIES: + print(code) + print('\n') + sys.exit(1) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-i', '--source-file', required = True, help = 'This is the source file path. The source file must contain one of the ONCOTREE_CODE, NCIT_CODE, UMLS_CODE, ICDO_TOPOGRAPHY_CODE, ICDO_MORPHOLOGY_CODE or HEMEONC_CODE in the file header and it must contain codes corresponding to the Ontology System.', type = str) + parser.add_argument('-o', '--target-file', required = True, help = 'This is the path to the target file that will be generated. It will contain the ontology mappings of source code in to .', type = str) + parser.add_argument('-s', '--source-code', required = True, help = "This is the source ontology code in . It must be one of the ONCOTREE_CODE, NCIT_CODE, UMLS_CODE, ICDO_TOPOGRAPHY_CODE, ICDO_MORPHOLOGY_CODE or HEMEONC_CODE.", type = str) + parser.add_argument('-t', '--target-code', required = True, help = "This is the target ontology code that the script will attempt to map the source ontology code to. It must be one of the ONCOTREE_CODE, NCIT_CODE, UMLS_CODE, ICDO_TOPOGRAPHY_CODE, ICDO_MORPHOLOGY_CODE or HEMEONC_CODE.", type = str) + args = parser.parse_args() + + target_file = args.target_file + source_code = args.source_code.upper() + target_code = args.target_code.upper() + + source_file = pd.read_csv(args.source_file, comment='#', sep='\t', header=0, keep_default_na=False).applymap(str) #HEMEONC_CODES are numbers. + validate_arguments(source_file, source_code, target_code) + + oncotree_code_source_data = requests.get(ONTOLOGY_MAPPINGS_FILE_URL).content + mappings_file = pd.read_csv(io.StringIO(oncotree_code_source_data.decode('utf-8')), sep='\t', header=0, keep_default_na=False).applymap(str) + + #For case insensitive merge. + source_file[source_code] = source_file[source_code].str.upper() + mappings_file[source_code] = mappings_file[source_code].str.upper() + + mapped_data = pd.merge(source_file, mappings_file[[source_code, target_code]], on=source_code, suffixes= ('_source_file', ''), sort=False, how='left').fillna('') + print("Mapped the %s to %s.." % (source_code, target_code)) + + add_comments_column_and_log_data(mapped_data, target_file, source_code, target_code, source_file) + print("\nThe ontology mappings are written to: %s" % (target_file)) + print("The mapping summary is written to : %s" % (os.path.splitext(target_file)[0] + "_summary.html")) + +if __name__ == '__main__': + main() diff --git a/scripts/test/__init__.py b/scripts/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/test/test_oncotree_to_oncotree.py b/scripts/test/test_oncotree_to_oncotree.py new file mode 100644 index 00000000..4e52012a --- /dev/null +++ b/scripts/test/test_oncotree_to_oncotree.py @@ -0,0 +1,265 @@ +# run all unit tests with: +# scripts> python -m unittest discover +# +# Author: Manda Wilson + +import unittest + +from oncotree_to_oncotree import * + +class TestCrossVersionOncotreeTranslator(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.original_version = cls.get_original_version() + for code in (cls.original_version): + GLOBAL_LOG_MAP[code] = { + NEIGHBORS_FIELD : [], + CHOICES_FIELD : [], + CLOSEST_COMMON_PARENT_FIELD : "", + IS_LOGGED_FLAG : False + } + cls.latest_version = cls.get_latest_version() + for code in (cls.latest_version): + GLOBAL_LOG_MAP[code] = { + NEIGHBORS_FIELD : [], + CHOICES_FIELD : [], + CLOSEST_COMMON_PARENT_FIELD : "", + IS_LOGGED_FLAG : False + } + + # --------------------------------------------------------------------------------- + # Original Code: SEZS (Child: FAKE_OLD_SS_CHILD) + # New Code: SS Child: FAKE_OLD_SS_CHILD, FAKE_NEW_SS_CHILD) + # Code is mapped through history. One new child is introduced in the future version (SS) + # Tests simple direct mapping with and without introduction of child (forward, backward) + + # get possible codes (jumping forward through history) + def test_get_possible_oncotree_code_forwards_history(self): + self.run_get_possible_oncotree_code_test("SEZS", set(["SS"]), False) + + # get possible codes (jumping backwards through history) + def test_get_possible_oncotree_code_backwards_history(self): + self.run_get_possible_oncotree_code_test("SS", set(["SEZS"]), True) + + # get resolved string - single direct mapping, new children available + def test_resolve_single_future_possible_target_oncotree_code_with_new_child(self): + self.run_resolve_oncotree_codes_test("SEZS", set(["SS"]), False, format_oncotree_code_options("SEZS", "{SS}", 1), False) + + # get resolved string - single direct mapping, no new children + def test_resolve_single_past_possible_target_oncotree_code(self): + self.run_resolve_oncotree_codes_test("SS", set(["SEZS"]), True, "SEZS", True) + + def test_get_number_of_new_children_backwards(self): + self.run_get_number_of_new_children_test("SS", set(["SEZS"]), True, 0) + + def test_get_number_of_new_children_fowards(self): + self.run_get_number_of_new_children_test("SEZS", set(["SS"]), False, 1) + # --------------------------------------------------------------------------------- + # Original Code: ALL (Children: BALL, TALL, DALL) + # New Code: BLL, TLL, DLL (No children) + # Original code's children are precursors/history to new codes: precursor (BALL -> BLL, TALL -> TLL), history (DALL -> DLL) + # ALL is in BLL, TLL revocations NOT DLL + + # ALL forward should map to "BLL" and "TLL" (not "DLL"), reverse mapping should not return "ALL" + def test_get_possible_oncotree_code_forwards_two_revocations(self): + self.run_get_possible_oncotree_code_test("ALL", set(["BLL", "TLL"]), False) + + def test_get_possible_oncotree_code_forwards_revocation_and_precursor(self): + self.run_get_possible_oncotree_code_test("BALL", set(["BLL"]), False) + + def test_get_possible_oncotree_code_forwards_revocation_and_history(self): + self.run_get_possible_oncotree_code_test("DALL", set(["DLL"]), False) + + # testing revocation - "BLL" should not include "ALL" as option + def test_get_possible_oncotree_code_backwards_revocation_and_precursor(self): + self.run_get_possible_oncotree_code_test("BLL", set(["BALL"]), True) + + def test_get_possible_oncotree_code_backwards_revocation_and_history(self): + self.run_get_possible_oncotree_code_test("DLL", set(["DALL"]), True) + + def test_resolve_multiple_future_possible_target_oncotree_codes_no_new_children(self): + self.run_resolve_oncotree_codes_test("ALL", set(["BLL", "TLL"]), False, format_oncotree_code_options("ALL", "{BLL,TLL}", 0), False) + # --------------------------------------------------------------------------------- + # Original Code: GMUCM + # New Code: URMM + # Original code was revoked and replaced with URMM + # URMM is effectively a new node and cannot be mapped directly back + + def test_get_possible_oncotree_code_backwards_revocation(self): + self.run_get_possible_oncotree_code_test("URMM", set([]), True) + + def test_get_possible_oncotree_code_forwards_revocations(self): + self.run_get_possible_oncotree_code_test("GMUCM", set(["URMM"]), False) + + # going backwards - no choices, search neighborhood - skip BLADDER because not mappable - map non-immediate neighbor TISSUE + def test_resolve_no_past_possible_target_oncotree_codes(self): + self.run_resolve_oncotree_codes_test("URMM", set(), True, format_oncotree_code_options("URMM", "Neighborhood: TISSUE", 0), False) + # --------------------------------------------------------------------------------- + # Original Code: CLL, SLL + # New Code: CLLSLL + # Original code was combined into CLLSLL (both are precursors) + # CLLSLL can be mapped back to either + + def test_get_possible_oncotree_code_backwards_merged_precusors(self): + self.run_get_possible_oncotree_code_test("CLLSLL", set(["CLL", "SLL"]), True) + + def test_get_possible_oncotree_code_forwards_merged_precusors(self): + self.run_get_possible_oncotree_code_test("CLL", set(["CLLSLL"]), False) + + def test_resolve_multiple_past_possible_target_oncotree_codes(self): + self.run_resolve_oncotree_codes_test("CLLSLL", set(["CLL", "SLL"]), True, format_oncotree_code_options("CLLSLL", "{CLL,SLL}", 0), False) + # --------------------------------------------------------------------------------- + # Original Code: PTCL, PTCLNOS + # New Code: PTCL (originally PTCLNOS) + # PTCL (the meaning) was revoked - PTCLNOS (the meaning) stayed but was renamed to PTCL + + def test_get_possible_oncotree_code_forwards_revoked_and_renamed(self): + self.run_get_possible_oncotree_code_test("PTCL", set(["PTCL"]), False) + + def test_get_possible_oncotree_code_forwards_revoked_and_renamed(self): + self.run_get_possible_oncotree_code_test("PTCLNOS", set(["PTCL"]), False) + + # in this test, we see that the revoked parent node (PTCL) is not chosen as a valid backwards mapping. Only the history (PTCLNOS) of the test node (PTCL) is considered valid + def test_get_possible_oncotree_code_backwards_revoked_and_renamed(self): + self.run_get_possible_oncotree_code_test("PTCL", set(["PTCLNOS"]), True) + # --------------------------------------------------------------------------------- + # Original Code: TNKL (parent), CTCL, TNKL_CHILD, PTCL (children) + # New Code: TNKL gone, CTCL renmaed to MYCF, PTCL revoked by PTCLNOS (renamed PTCL), TNKL_CHILD renamed to TNKL + # TNKL can't be mapped forward - possible set should be 0 + + # Tests for TNKL + def test_get_no_possible_oncotree_code_forwards_multiple_children(self): + self.run_get_possible_oncotree_code_test("TNKL", set(), False) + + def test_resolve_multiple_past_possible_target_oncotree_codes(self): + self.run_resolve_oncotree_codes_test("TNKL", set(), False, format_oncotree_code_options("TNKL", "Neighborhood: MYCF,PTCL,TISSUE,TNKL_CHILD2", 0), False) + + def test_get_possible_oncotree_code_forwards_precursors(self): + self.run_get_possible_oncotree_code_test("CTCL", set(["MYCF"]), False) + + def test_get_possible_oncotree_code_backwards_precursors(self): + self.run_get_possible_oncotree_code_test("MYCF", set(["CTCL"]), True) + # --------------------------------------------------------------------------------- + # Tests for getting valid neighboring OncoTree codes + + # TNKL_NEW_CHILD parent MTNN is not in target (original) version + # TNKL_GRANDCHILD is in both + def test_get_neighbor_invalid_parent_valid_children(self): + self.run_get_neighboring_target_oncotree_codes_test(["TNKL_NEW_CHILD"], True, set(["TNKL_GRANDCHILD"])) + + # TNKL_NEW_CHILD2 parent MTNN is not in target (original) version + # skip to grandparent "TISSUE" + # no children + def test_get_neighbor_invalid_parent_invalid_children(self): + self.run_get_neighboring_target_oncotree_codes_test(["TNKL_NEW_CHILD2"], True, set(["TISSUE"])) + + # FAKE_NEW_SS_CHILD has no children + # parent ("SS") maps back to "SEZS" in target version + def test_get_neighbor_valid_parent_invalid_children(self): + self.run_get_neighboring_target_oncotree_codes_test(["FAKE_NEW_SS_CHILD"], True, set(["SEZS"])) + + # BLADDER has children CLLSLL, URMM + # URMM revoked GMUCM (GMUCM is no longer valid) - don't map back + # CLLSLL maps back to CLL or SLL + def test_get_neighbor_partially_valid_children_valid_parent(self): + self.run_get_neighboring_target_oncotree_codes_test(["BLADDER"], True, set(["CLL", "SLL", "TISSUE"])) + # --------------------------------------------------------------------------------- + # Tests for getting closest common parent for a set of nodes + + def test_get_closest_common_parent_for_distant_nodes(self): + self.run_get_closest_common_parent_test(["TNKL_GRANDCHILD", "CLL", "PTCLNOS", "MEL"], self.original_version, "TISSUE") + + def test_get_closest_common_parent_for_parent_child_nodes(self): + self.run_get_closest_common_parent_test(["SEZS", "CTCL", "FAKE_OLD_SS_CHILD"], self.original_version, "CTCL") + + def test_get_closest_common_parent_for_related_child_nodes(self): + self.run_get_closest_common_parent_test(["TALL", "BALL", "DALL"], self.original_version, "ALL") + # --------------------------------------------------------------------------------- + # Test functions + def run_get_possible_oncotree_code_test(self, test_oncotree_code, expected_possible_oncotree_codes, is_backwards_mapping): + source_version = self.original_version if not is_backwards_mapping else self.latest_version + target_version = self.latest_version if not is_backwards_mapping else self.original_version + actual_output = get_possible_target_oncotree_codes(source_version[test_oncotree_code], target_version, is_backwards_mapping) + self.assertEqual(expected_possible_oncotree_codes, actual_output) + + def run_resolve_oncotree_codes_test(self, source_oncotree_code, possible_target_oncotree_codes, is_backwards_mapping, expected_oncotree_code_option, expected_is_easily_resolved): + source_version = self.original_version if not is_backwards_mapping else self.latest_version + target_version = self.latest_version if not is_backwards_mapping else self.original_version + actual_oncotree_code_option, actual_is_easily_resolved = resolve_possible_target_oncotree_codes(source_oncotree_code, possible_target_oncotree_codes, source_version, target_version, is_backwards_mapping) + self.assertEqual(expected_is_easily_resolved, actual_is_easily_resolved) + # since neighborhood returns a set with no order, check contents instead + # XYZ -> Neighborhood: X,Y,Z + # split ['XYZ -> Neighborhood', 'X,Y,Z'] + # split ['X,Y,Z'] to ['X', 'Y', 'Z'] + if "Neighborhood" in expected_oncotree_code_option: + expected_oncotree_codes = expected_oncotree_code_option.split("Neighborhood: ")[1].split(",") + actual_oncotree_codes = actual_oncotree_code_option.split("Neighborhood: ")[1].split(",") + self.assertEqual(set(expected_oncotree_codes), set(actual_oncotree_codes)) + else: + self.assertEqual(expected_oncotree_code_option, actual_oncotree_code_option) + + def run_get_neighboring_target_oncotree_codes_test(self, source_oncotree_codes, is_backwards_mapping, expected_neighbors): + source_version = self.original_version if not is_backwards_mapping else self.latest_version + target_version = self.latest_version if not is_backwards_mapping else self.original_version + actual_neighbors = get_neighboring_target_oncotree_codes(source_oncotree_codes, source_version, target_version, True, is_backwards_mapping) + self.assertEqual(expected_neighbors, actual_neighbors) + + def run_get_number_of_new_children_test(self, source_oncotree_code, possible_target_oncotree_codes, is_backwards_mapping, expected_number_of_new_children): + source_version = self.original_version if not is_backwards_mapping else self.latest_version + target_version = self.latest_version if not is_backwards_mapping else self.original_version + actual_number_of_new_children = get_number_of_new_children(source_oncotree_code, possible_target_oncotree_codes, source_version, target_version) + self.assertEqual(expected_number_of_new_children, actual_number_of_new_children) + + def run_get_closest_common_parent_test(self, possible_target_oncotree_codes, target_oncotree, expected_closest_common_parent): + actual_closest_common_parent = get_closest_common_parent(possible_target_oncotree_codes, target_oncotree) + self.assertEqual(expected_closest_common_parent, actual_closest_common_parent) + + @classmethod + def get_original_version(cls): + return { + "ALL" : {"code": "ALL", "children": ["TALL", "BALL", "DALL"], "parent": "LEUK", "revocations": [], "precursors": [], "history": []}, + "BALL" : {"code": "BALL", "children": [], "parent": "ALL", "revocations": [], "precursors": [], "history": []}, + "CLL" : {"code": "CLL", "children": [], "parent": "LEUK", "revocations": [], "precursors": [], "history": []}, + "CTCL" : {"code": "CTCL", "children": ["SEZS"], "parent": "TNKL", "revocations": [], "precursors": [], "history": []}, + "DALL" : {"code": "DALL", "children": [], "parent": "ALL", "revocations": [], "precursors": [], "history": []}, + "FAKE_OLD_SS_CHILD" : {"code": "FAKE_OLD_SS_CHILD", "children": [], "parent": "SEZS", "revocations": [], "precursors": [], "history": [""]}, + "GMUCM" : {"code": "GMUCM", "children": [], "parent": "MEL", "revocations": [], "precursors": [], "history": []}, + "LEUK" : {"code": "LEUK", "children": ["ALL", "CLL"], "parent": "TISSUE", "revocations": [], "precursors": [], "history": []}, + "MEL" : {"code": "MEL", "children": ["GMUCM", "SLL"], "parent": "TISSUE", "revocations": [], "precursors": [], "history": []}, + "PTCL" : {"code": "PTCL", "children": ["PCTLNOS"], "parent": "TNKL", "revocations": [], "precursors": [], "history": []}, + "PTCLNOS" : {"code": "PTCLNOS", "children": [], "parent": "PTCL", "revocations": [], "precursors": [], "history": []}, + "SEZS" : {"code": "SEZS", "children": ["FAKE_OLD_SS_CHILD"], "parent": "CTCL", "revocations": [], "precursors": [], "history": []}, + "SLL" : {"code": "SLL", "children": [], "parent": "MEL", "revocations": [], "precursors": [], "history": []}, + "TALL" : {"code": "TALL", "children": [], "parent": "ALL", "revocations": [], "precursors": [], "history": []}, + "TISSUE" : {"code": "TISSUE", "children": ["LEUK", "MEL", "TNKL"], "parent": "", "revocations": [], "precursors": [], "history": []}, + "TNKL" : {"code": "TNKL", "children": ["PTCL", "CTCL", "TNKL_CHILD"], "parent": "TISSUE", "revocations": [], "precursors": [], "history": []}, + "TNKL_CHILD" : {"code": "TNKL_CHILD", "children": [], "parent": "TNKL", "revocations": [], "precursors": [], "history": []}, + "TNKL_GRANDCHILD" : {"code": "TNKL_GRANDCHILD", "children": [], "parent": "TNKL_CHILD", "revocations": [], "precursors": [], "history": []} + } + + @classmethod + def get_latest_version(cls): + return { + "BLADDER" : {"code": "BLADDER", "children": ["URMM", "CLLSLL"], "parent": "TISSUE", "revocations": [], "precursors": [], "history": []}, + "BLL" : {"code": "BLL", "children": [], "parent": "LNM", "revocations": ["ALL"], "precursors": ["BALL"], "history": []}, + "CLLSLL" : {"code": "CLLSLL", "children": [], "parent": "BLADDER", "revocations": [], "precursors": ["SLL", "CLL"], "history": []}, + "DLL" : {"code": "DLL", "children": [], "parent": "LNM", "revocations": [], "precursors": [], "history": ["DALL"]}, + "FAKE_NEW_SS_CHILD" : {"code": "FAKE_NEW_SS_CHILD", "children": [], "parent": "SS", "revocations": [], "precursors": [], "history": [""]}, + "FAKE_OLD_SS_CHILD" : {"code": "FAKE_OLD_SS_CHILD", "children": [], "parent": "SS", "revocations": [], "precursors": [], "history": [""]}, + "LNM" : {"code": "LNM", "children": ["BLL"], "parent": "TISSUE", "revocations": [], "precursors": [], "history": []}, + "MTNN" : {"code": "MTNN", "children": ["SS", "PTCL", "MYCF"], "parent": "TISSUE", "revocations": [], "precursors": [], "history": []}, + "MYCF" : {"code": "MYCF", "children": [], "parent": "MTNN", "revocations": [], "precursors": ["CTCL"], "history": []}, + "PTCL" : {"code": "PTCL", "children": [], "parent": "MTNN", "revocations": ["PTCL"], "precursors": [], "history": ["PTCLNOS"]}, + "SS" : {"code": "SS", "children": ["FAKE_NEW_SS_CHILD", "FAKE_OLD_SS_CHILD"], "parent": "MTNN", "revocations": [], "precursors": [], "history": ["SEZS"]}, + "TLL" : {"code": "TLL", "children": [], "parent": "LNM", "revocations": ["ALL"], "precursors": ["TALL"], "history": []}, + "TISSUE" : {"code": "TISSUE", "children": ["MTNN", "BLADDER", "LNM"], "parent": "", "revocations": [], "precursors": [], "history": []}, + "TNKL_GRANDCHILD" : {"code": "TNKL_GRANDCHILD", "children": [], "parent": "TNKL_NEW_CHILD", "revocations": [], "precursors": [], "history": []}, + "TNKL_NEW_CHILD" : {"code": "TNKL_NEW_CHILD", "children": ["TNKL_GRANDCHILD"], "parent": "MTNN", "revocations": [], "precursors": [], "history": [""]}, + "TNKL_NEW_CHILD2" : {"code": "TNKL_NEW_CHILD2", "children": [], "parent": "MTNN", "revocations": [], "precursors": [], "history": [""]}, + "TNKL_CHILD2" : {"code": "TNKL_CHILD2", "children": ["TNKL_GRANDCHILD"], "parent": "MTNN", "revocations": [], "precursors": [], "history": ["TNKL_CHILD"]}, + "URMM" : {"code": "URMM", "children": [], "parent": "BLADDER", "revocations": ["GMUCM"], "precursors": [], "history": []} + } + +if __name__ == '__main__': + unittest.main()