From d3ff7f02c3e772bc262fd32c7779ab4189eb7f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=C3=A9cio=20Santos?= Date: Wed, 5 Jun 2024 20:55:06 -0400 Subject: [PATCH 01/22] feat(api): Add new function bdi.match_columns() --- bdikit/__init__.py | 1 + bdikit/functional_api.py | 75 ++++++++++++++++++++++++++++++++++++++++ tests/test_api.py | 58 +++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 bdikit/functional_api.py diff --git a/bdikit/__init__.py b/bdikit/__init__.py index 79d0deeb..f2c6481d 100644 --- a/bdikit/__init__.py +++ b/bdikit/__init__.py @@ -1,3 +1,4 @@ __version__ = "0.2.0.dev0" # To shortcut the import path from bdikit.api import APIManager +from bdikit.functional_api import * diff --git a/bdikit/functional_api.py b/bdikit/functional_api.py new file mode 100644 index 00000000..d7eddf32 --- /dev/null +++ b/bdikit/functional_api.py @@ -0,0 +1,75 @@ +from enum import Enum +from os.path import join, dirname +from typing import Union, Type +import pandas as pd +from bdikit.mapping_algorithms.column_mapping.algorithms import ( + BaseColumnMappingAlgorithm, + SimFloodAlgorithm, + ComaAlgorithm, + CupidAlgorithm, + DistributionBasedAlgorithm, + JaccardDistanceAlgorithm, + GPTAlgorithm, +) + +GDC_DATA_PATH = join(dirname(__file__), "./resource/gdc_table.csv") + + +class ColumnMappingMethod(Enum): + SIMFLOOD = ("similarity_flooding", SimFloodAlgorithm) + COMA = ("coma", ComaAlgorithm) + CUPID = ("cupid", CupidAlgorithm) + DISTRIBUTION_BASED = ("distribution_based", DistributionBasedAlgorithm) + JACCARD_DISTANCE = ("jaccard_distance", JaccardDistanceAlgorithm) + GPT = ("gpt", GPTAlgorithm) + + def __init__( + self, method_name: str, method_class: Type[BaseColumnMappingAlgorithm] + ): + self.method_name = method_name + self.method_class = method_class + + @staticmethod + def get_class(method_name: str): + methods = { + method.method_name: method.method_class for method in ColumnMappingMethod + } + try: + return methods[method_name] + except KeyError: + names = ", ".join(list(methods.keys())) + raise ValueError( + f"The {method_name} algorithm is not supported. " + f"Supported algorithms are: {names}" + ) + + +def match_columns( + source: pd.DataFrame, + target: Union[str, pd.DataFrame] = "gdc", + method: str = ColumnMappingMethod.SIMFLOOD.name, +) -> pd.DataFrame: + """ + Performs schema mapping between the source table and the given target. The target + either is a DataFrame or a string representing a standard data vocabulary. + """ + if isinstance(target, str): + target_table = _load_table_for_standard(target) + else: + target_table = target + + matcher_instance = ColumnMappingMethod.get_class(method)(source, target_table) + matches = matcher_instance.map() + + return pd.DataFrame(matches.items(), columns=["source", "target"]) + + +def _load_table_for_standard(name: str) -> pd.DataFrame: + """ + Load the table for the given standard data vocabulary. Currently, only the + GDC standard is supported. + """ + if name == "gdc": + return pd.read_csv(GDC_DATA_PATH) + else: + raise ValueError(f"The {name} standard is not supported") diff --git a/tests/test_api.py b/tests/test_api.py index e69de29b..889b987c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -0,0 +1,58 @@ +import bdikit as bdi +import pandas as pd + + +def test_bdi_match_columns_with_dataframes(): + # given + source = pd.DataFrame({"column_1": ["a1", "b1", "c1"], "col_2": ["a2", "b2", "c2"]}) + target = pd.DataFrame({"column_1a": ["a1", "b1", "c1"], "col2": ["a2", "b2", "c2"]}) + + # when + df_matches = bdi.match_columns(source, target=target, method="similarity_flooding") + + # then assert that the df_matches contains a row with the value 'column_1' + # in the column 'source' and the value 'column_1a' in the 'target' value + assert "source" in df_matches.columns + assert "target" in df_matches.columns + + df_filter = df_matches["source"] == "column_1" + assert df_matches[df_filter]["target"].values[0] == "column_1a" + + df_filter = df_matches["source"] == "col_2" + assert df_matches[df_filter]["target"].values[0] == "col2" + + +def test_bdi_match_columns_to_gdc(): + # given + source = pd.DataFrame( + { + "FIGO_stage": [ + "Stage 0", + "Stage I", + "Stage IA", + "Stage IA1", + "Stage IA2", + ], + "Ethnicity": [ + "Not-Hispanic or Latino", + "Hispanic or Latino", + "Not reported", + "Hispanic or Latino", + "Hispanic or Latino", + ], + } + ) + + # when + df_matches = bdi.match_columns(source, target="gdc", method="coma") + + # then df_matches must contain target columns that come from the GDC dictionary + assert df_matches.empty == False + assert "source" in df_matches.columns + assert "target" in df_matches.columns + + df_filter = df_matches["source"] == "Ethnicity" + assert df_matches[df_filter]["target"].values[0] == "ethnicity" + + df_filter = df_matches["source"] == "FIGO_stage" + assert df_matches[df_filter]["target"].values[0] == "figo_stage" From ca8f0b89fbe0beb688b8b90330a5bb06e13e5f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=C3=A9cio=20Santos?= Date: Thu, 6 Jun 2024 18:16:01 -0400 Subject: [PATCH 02/22] feat(api): Add new function bdi.top_matches() --- bdikit/functional_api.py | 42 +++++++++++- .../contrastive_learning/cl_api.py | 14 +++- tests/test_api.py | 68 +++++++++++++++++++ 3 files changed, 120 insertions(+), 4 deletions(-) diff --git a/bdikit/functional_api.py b/bdikit/functional_api.py index d7eddf32..44c354ae 100644 --- a/bdikit/functional_api.py +++ b/bdikit/functional_api.py @@ -1,7 +1,8 @@ from enum import Enum from os.path import join, dirname -from typing import Union, Type +from typing import Union, Type, List, Optional import pandas as pd +from bdikit.download import get_cached_model_or_download from bdikit.mapping_algorithms.column_mapping.algorithms import ( BaseColumnMappingAlgorithm, SimFloodAlgorithm, @@ -11,6 +12,9 @@ JaccardDistanceAlgorithm, GPTAlgorithm, ) +from bdikit.mapping_algorithms.scope_reducing._algorithms.contrastive_learning.cl_api import ( + ContrastiveLearningAPI, +) GDC_DATA_PATH = join(dirname(__file__), "./resource/gdc_table.csv") @@ -73,3 +77,39 @@ def _load_table_for_standard(name: str) -> pd.DataFrame: return pd.read_csv(GDC_DATA_PATH) else: raise ValueError(f"The {name} standard is not supported") + + +def top_matches( + source: pd.DataFrame, + columns: Optional[List[str]] = None, + target: Union[str, pd.DataFrame] = "gdc", + top_k: int = 10, +) -> pd.DataFrame: + """ + Returns the top-k matches between the source and target tables. + """ + + if isinstance(target, str): + target_table = _load_table_for_standard(target) + else: + target_table = target + + if columns is not None and len(columns) > 0: + selected_columns = source[columns] + else: + selected_columns = source + + model_path = get_cached_model_or_download("cl-reducer-v0.1") + api = ContrastiveLearningAPI(model_path=model_path, top_k=top_k) + _, scopes_json = api.get_recommendations(selected_columns, target=target_table) + + dfs = [] + for scope in scopes_json: + matches = pd.DataFrame( + scope["Top k columns"], columns=["matches", "similarity"] + ) + matches["source"] = scope["Candidate column"] + matches = matches[["source", "matches", "similarity"]] + dfs.append(matches.sort_values(by="similarity", ascending=False)) + + return pd.concat(dfs, ignore_index=True) diff --git a/bdikit/mapping_algorithms/scope_reducing/_algorithms/contrastive_learning/cl_api.py b/bdikit/mapping_algorithms/scope_reducing/_algorithms/contrastive_learning/cl_api.py index 4d7d10b6..2a079b9d 100644 --- a/bdikit/mapping_algorithms/scope_reducing/_algorithms/contrastive_learning/cl_api.py +++ b/bdikit/mapping_algorithms/scope_reducing/_algorithms/contrastive_learning/cl_api.py @@ -1,5 +1,5 @@ import os -from typing import List +from typing import List, Optional, Union import numpy as np import pandas as pd @@ -37,8 +37,16 @@ def load_checkpoint(self, lm="roberta"): return model - def get_recommendations(self, table: pd.DataFrame): - gdc_ds = pd.read_csv(GDC_TABLE_PATH) + def get_recommendations( + self, table: pd.DataFrame, target: Optional[Union[str, pd.DataFrame]] = None + ): + if target is None or (isinstance(target, str) and target == "gdc"): + gdc_ds = pd.read_csv(GDC_TABLE_PATH) + elif isinstance(target, pd.DataFrame): + gdc_ds = target + else: + raise ValueError("Target must be a DataFrame or 'gdc'") + l_features = self._load_table_tokens(table) r_features = self._load_table_tokens(gdc_ds) cosine_sim = cosine_similarity(l_features, r_features) diff --git a/tests/test_api.py b/tests/test_api.py index 889b987c..ff641a33 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -56,3 +56,71 @@ def test_bdi_match_columns_to_gdc(): df_filter = df_matches["source"] == "FIGO_stage" assert df_matches[df_filter]["target"].values[0] == "figo_stage" + + +def test_bdi_top_matches_with_dataframes(): + # given + source = pd.DataFrame({"tumor_size": ["a1", "b1", "c1"]}) + target = pd.DataFrame( + { + "color": ["red", "blue", "green", "yellow"], + "tumor_length": [12, 23, 34, 45], + "tumor_magnitude": [12, 23, 34, 45], + "tumor_size": [12, 23, 34, 45], + } + ) + + # when + df_matches = bdi.top_matches(source, target=target, top_k=3) + + # then + assert len(df_matches.index) == 3 + assert "source" in df_matches.columns + assert "matches" in df_matches.columns + assert "similarity" in df_matches.columns + + df_filter = df_matches["source"] == "tumor_size" + assert "tumor_size" in df_matches[df_filter]["matches"].tolist() + assert "tumor_magnitude" in df_matches[df_filter]["matches"].tolist() + assert "tumor_length" in df_matches[df_filter]["matches"].tolist() + + +def test_bdi_top_matches_gdc(): + # given + source = pd.DataFrame( + { + "FIGO_stage": [ + "Stage 0", + "Stage I", + "Stage IA", + "Stage IA1", + "Stage IA2", + ], + "Ethnicity": [ + "Not-Hispanic or Latino", + "Hispanic or Latino", + "Not reported", + "Hispanic or Latino", + "Hispanic or Latino", + ], + } + ) + + # when + df_matches = bdi.top_matches(source, target="gdc", top_k=5) + + # then + assert df_matches.empty == False + assert "source" in df_matches.columns + assert "matches" in df_matches.columns + assert "similarity" in df_matches.columns + + df_filter = df_matches["source"] == "FIGO_stage" + assert len(df_matches[df_filter]) == 5 + assert "figo_stage" in df_matches[df_filter]["matches"].tolist() + assert "uicc_clinical_stage" in df_matches[df_filter]["matches"].tolist() + + df_filter = df_matches["source"] == "Ethnicity" + assert len(df_matches[df_filter]) == 5 + assert "ethnicity" in df_matches[df_filter]["matches"].tolist() + assert "race" in df_matches[df_filter]["matches"].tolist() From ee9c09fdf2c04dce5f44aed622a03786c7a8aff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=C3=A9cio=20Santos?= Date: Fri, 7 Jun 2024 16:13:08 -0400 Subject: [PATCH 03/22] feat(api): Added bdi.materialize_mapping() and basic value mappers --- bdikit/functional_api.py | 23 +++++++ .../value_mapping/value_mappers.py | 62 +++++++++++++++++++ tests/test_api.py | 57 +++++++++++++++++ tests/test_value_mapping.py | 42 +++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 bdikit/mapping_algorithms/value_mapping/value_mappers.py create mode 100644 tests/test_value_mapping.py diff --git a/bdikit/functional_api.py b/bdikit/functional_api.py index 44c354ae..e7a76be9 100644 --- a/bdikit/functional_api.py +++ b/bdikit/functional_api.py @@ -12,6 +12,7 @@ JaccardDistanceAlgorithm, GPTAlgorithm, ) +from bdikit.mapping_algorithms.value_mapping.value_mappers import ValueMapper from bdikit.mapping_algorithms.scope_reducing._algorithms.contrastive_learning.cl_api import ( ContrastiveLearningAPI, ) @@ -113,3 +114,25 @@ def top_matches( dfs.append(matches.sort_values(by="similarity", ascending=False)) return pd.concat(dfs, ignore_index=True) + + +def materialize_mapping( + input_dataframe: pd.DataFrame, target: List[dict] +) -> pd.DataFrame: + output_dataframe = pd.DataFrame() + for mapping_spec in target: + from_column_name = mapping_spec["from"] + to_column_name = mapping_spec["to"] + value_mapper = mapping_spec["mapper"] + output_dataframe[to_column_name] = map_column_values( + input_dataframe[from_column_name], to_column_name, value_mapper + ) + return output_dataframe + + +def map_column_values( + input_column: pd.Series, target: str, value_mapper: ValueMapper +) -> pd.Series: + new_column = value_mapper.map(input_column) + new_column.name = target + return new_column diff --git a/bdikit/mapping_algorithms/value_mapping/value_mappers.py b/bdikit/mapping_algorithms/value_mapping/value_mappers.py new file mode 100644 index 00000000..4c36f4ad --- /dev/null +++ b/bdikit/mapping_algorithms/value_mapping/value_mappers.py @@ -0,0 +1,62 @@ +import pandas as pd + + +class ValueMapper: + """ + A ValueMapper represents objects that transform the values in a input + column to the values from a new output column. + """ + + def map(self, input_column: pd.Series) -> pd.Series: + """ + Every concrete ValueMapper should implement this method, which takes a + pandas Series as input and returns a new pandas Series with transformed + values. + """ + pass + + +class IdentityValueMapper(ValueMapper): + """ + A column mapper that maps each value in input column into itself. + """ + + def map(self, input_column: pd.Series) -> pd.Series: + """ + Simply copies the values in input_column to the output column. + """ + return input_column.copy() + + +class FunctionValueMapper(ValueMapper): + """ + A column mapper that transforms each value in the input column using the + provided custom function. + """ + + def __init__(self, function): + self.function = function + + def map(self, input_column: pd.Series) -> pd.Series: + """ + Applies the given function to each value in input_column to generate + the output column. + """ + return input_column.map(self.function) + + +class DictionaryMapper(ValueMapper): + """ + A column mapper that transforms each value in the input column using the + values stored in the provided dictionary. + """ + + def __init__(self, dictionary: dict): + self.dictionary = dictionary + + def map(self, input_column: pd.Series) -> pd.Series: + """ + Transforms the values in the input_column to the values specified in + the dictionary provided using the object constructor. + """ + return input_column.map(self.dictionary) diff --git a/tests/test_api.py b/tests/test_api.py index ff641a33..39248a1c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,5 +1,9 @@ import bdikit as bdi import pandas as pd +from bdikit.mapping_algorithms.value_mapping.value_mappers import ( + FunctionValueMapper, + IdentityValueMapper, +) def test_bdi_match_columns_with_dataframes(): @@ -124,3 +128,56 @@ def test_bdi_top_matches_gdc(): assert len(df_matches[df_filter]) == 5 assert "ethnicity" in df_matches[df_filter]["matches"].tolist() assert "race" in df_matches[df_filter]["matches"].tolist() + + +def test_map_column_values(): + """ + Ensures that the map_column_values function correctly maps the values of + a column and assings the target name. + """ + # given + str_column = pd.Series(data=["a", "b", "c", "d", "e"], name="column_str") + value_mapper = FunctionValueMapper(function=lambda x: x.upper()) + target_column_name = "string column" + + # when + mapped_column = bdi.map_column_values( + str_column, target=target_column_name, value_mapper=value_mapper + ) + + # then + upper_cased_values = ["A", "B", "C", "D", "E"] + assert mapped_column.name == target_column_name + assert mapped_column.eq(upper_cased_values).all() + + +def test_map_dataframe_column_values(): + # given + str_column_1 = ["a", "b", "c", "d", "e"] + str_column_2 = ["a", "b", "c", "d", "e"] + df_base = pd.DataFrame({"column_str_1": str_column_1, "column_str_2": str_column_2}) + + value_mapping_spec = [ + { + "from": "column_str_1", + "to": "string column 1", + "mapper": IdentityValueMapper(), + }, + { + "from": "column_str_2", + "to": "string column 2", + "mapper": FunctionValueMapper(function=lambda x: x.upper()), + }, + ] + + # when + df_mapped = bdi.materialize_mapping(df_base, target=value_mapping_spec) + + # then + assert len(df_mapped.columns) == 2 + + assert "string column 1" in df_mapped.columns + assert df_mapped["string column 1"].eq(str_column_1).all() + + assert "string column 2" in df_mapped.columns + assert df_mapped["string column 2"].eq(["A", "B", "C", "D", "E"]).all() diff --git a/tests/test_value_mapping.py b/tests/test_value_mapping.py new file mode 100644 index 00000000..ea782adb --- /dev/null +++ b/tests/test_value_mapping.py @@ -0,0 +1,42 @@ +import pandas as pd +from bdikit.mapping_algorithms.value_mapping.value_mappers import ( + FunctionValueMapper, + DictionaryMapper, + IdentityValueMapper, +) + + +def test_identity_mapper(): + # given + str_column = pd.Series(data=["a", "b", "c", "d", "e"], name="column_str") + identity_mapper = IdentityValueMapper() + + # when + mapped_column = identity_mapper.map(str_column) + + # then + assert mapped_column.eq(["a", "b", "c", "d", "e"]).all() + + +def test_dictionary_mapper(): + # given + str_column = pd.Series(data=["a", "b", "c", "d", "e"], name="column_str") + dict_mapper = DictionaryMapper(dictionary={"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}) + + # when + mapped_column = dict_mapper.map(str_column) + + # then + assert mapped_column.eq([1, 2, 3, 4, 5]).all() + + +def test_custom_function_mapper(): + # given + str_column = pd.Series(data=["a", "b", "c", "d", "e"], name="column_str") + fn_mapper = FunctionValueMapper(function=lambda x: x + x) + + # when + mapped_column = fn_mapper.map(str_column) + + # then + assert mapped_column.eq(["aa", "bb", "cc", "dd", "ee"]).all() From adae6d74e94cd192f4648f865b7f2a99bd7783c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=C3=A9cio=20Santos?= Date: Fri, 7 Jun 2024 18:05:19 -0400 Subject: [PATCH 04/22] Change column matching API to be stateless --- bdikit/functional_api.py | 8 ++-- .../column_mapping/algorithms.py | 46 ++++++++----------- tests/test_column_mapping.py | 4 +- 3 files changed, 25 insertions(+), 33 deletions(-) diff --git a/bdikit/functional_api.py b/bdikit/functional_api.py index e7a76be9..9859e504 100644 --- a/bdikit/functional_api.py +++ b/bdikit/functional_api.py @@ -35,12 +35,12 @@ def __init__( self.method_class = method_class @staticmethod - def get_class(method_name: str): + def get_instance(method_name: str) -> BaseColumnMappingAlgorithm: methods = { method.method_name: method.method_class for method in ColumnMappingMethod } try: - return methods[method_name] + return methods[method_name]() except KeyError: names = ", ".join(list(methods.keys())) raise ValueError( @@ -63,8 +63,8 @@ def match_columns( else: target_table = target - matcher_instance = ColumnMappingMethod.get_class(method)(source, target_table) - matches = matcher_instance.map() + matcher_instance = ColumnMappingMethod.get_instance(method) + matches = matcher_instance.map(source, target_table) return pd.DataFrame(matches.items(), columns=["source", "target"]) diff --git a/bdikit/mapping_algorithms/column_mapping/algorithms.py b/bdikit/mapping_algorithms/column_mapping/algorithms.py index 4ac5db56..f6788b36 100644 --- a/bdikit/mapping_algorithms/column_mapping/algorithms.py +++ b/bdikit/mapping_algorithms/column_mapping/algorithms.py @@ -14,23 +14,16 @@ class BaseColumnMappingAlgorithm: - def __init__(self, dataset, global_table): - self._dataset = dataset - self._global_table = global_table - - def map(self) -> Dict[str, str]: + def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame) -> Dict[str, str]: raise NotImplementedError("Subclasses must implement this method") class ValentineColumnMappingAlgorithm(BaseColumnMappingAlgorithm): - def __init__(self, dataset, global_table, matcher: BaseMatcher): - super().__init__(dataset, global_table) + def __init__(self, matcher: BaseMatcher): self.matcher = matcher - def map(self) -> Dict[str, str]: - matches: MatcherResults = valentine_match( - self._dataset, self._global_table, self.matcher - ) + def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame) -> Dict[str, str]: + matches: MatcherResults = valentine_match(dataset, global_table, self.matcher) mappings = {} for match in matches.one_to_one(): dataset_candidate = match[0][1] @@ -40,42 +33,41 @@ def map(self) -> Dict[str, str]: class SimFloodAlgorithm(ValentineColumnMappingAlgorithm): - def __init__(self, dataset, global_table): - super().__init__(dataset, global_table, SimilarityFlooding()) + def __init__(self): + super().__init__(SimilarityFlooding()) class ComaAlgorithm(ValentineColumnMappingAlgorithm): - def __init__(self, dataset, global_table): - super().__init__(dataset, global_table, Coma()) + def __init__(self): + super().__init__(Coma()) class CupidAlgorithm(ValentineColumnMappingAlgorithm): - def __init__(self, dataset, global_table): - super().__init__(dataset, global_table, Cupid()) + def __init__(self): + super().__init__(Cupid()) class DistributionBasedAlgorithm(ValentineColumnMappingAlgorithm): - def __init__(self, dataset, global_table): - super().__init__(dataset, global_table, DistributionBased()) + def __init__(self): + super().__init__(DistributionBased()) class JaccardDistanceAlgorithm(ValentineColumnMappingAlgorithm): - def __init__(self, dataset, global_table): - super().__init__(dataset, global_table, JaccardDistanceMatcher()) + def __init__(self): + super().__init__(JaccardDistanceMatcher()) class GPTAlgorithm(BaseColumnMappingAlgorithm): - def __init__(self, dataset, global_table): - super().__init__(dataset, global_table) + def __init__(self): self.client = OpenAI() - def map(self): - global_columns = self._global_table.columns + def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame): + global_columns = global_table.columns labels = ", ".join(global_columns) - candidate_columns = self._dataset.columns + candidate_columns = dataset.columns mappings = {} for column in candidate_columns: - col = self._dataset[column] + col = dataset[column] values = col.drop_duplicates().dropna() if len(values) > 15: rows = values.sample(15).tolist() diff --git a/tests/test_column_mapping.py b/tests/test_column_mapping.py index c8755712..f8a04188 100644 --- a/tests/test_column_mapping.py +++ b/tests/test_column_mapping.py @@ -25,10 +25,10 @@ def test_basic_column_mapping_algorithms(self): table2 = pd.DataFrame( {"column_1a": ["a1", "b1", "c1"], "col2": ["a2", "b2", "c2"]} ) - column_matcher = ColumnMatcher(dataset=table1, global_table=table2) + column_matcher = ColumnMatcher() # when - mapping = column_matcher.map() + mapping = column_matcher.map(dataset=table1, global_table=table2) # then print(mapping) From bc7976406b93634dc3b6f38f02ec5a8d958439f6 Mon Sep 17 00:00:00 2001 From: EduardoPena Date: Mon, 10 Jun 2024 17:23:43 -0400 Subject: [PATCH 05/22] Adding TwoPhase ColumnMatch algorithm based on CTLearning --- bdikit/functional_api.py | 4 + .../column_mapping/algorithms.py | 59 +- examples/column_matching.ipynb | 983 ++++++++++++++++++ 3 files changed, 1042 insertions(+), 4 deletions(-) create mode 100644 examples/column_matching.ipynb diff --git a/bdikit/functional_api.py b/bdikit/functional_api.py index 9859e504..48fcaa6a 100644 --- a/bdikit/functional_api.py +++ b/bdikit/functional_api.py @@ -11,6 +11,8 @@ DistributionBasedAlgorithm, JaccardDistanceAlgorithm, GPTAlgorithm, + ContrastiveLearningAlgorithm, + TwoPhaseMatcherAlgorithm ) from bdikit.mapping_algorithms.value_mapping.value_mappers import ValueMapper from bdikit.mapping_algorithms.scope_reducing._algorithms.contrastive_learning.cl_api import ( @@ -27,6 +29,8 @@ class ColumnMappingMethod(Enum): DISTRIBUTION_BASED = ("distribution_based", DistributionBasedAlgorithm) JACCARD_DISTANCE = ("jaccard_distance", JaccardDistanceAlgorithm) GPT = ("gpt", GPTAlgorithm) + CT_LEARGNING = ("ct_learning", ContrastiveLearningAlgorithm) + TWO_PHASE = ("two_phase", TwoPhaseMatcherAlgorithm) def __init__( self, method_name: str, method_class: Type[BaseColumnMappingAlgorithm] diff --git a/bdikit/mapping_algorithms/column_mapping/algorithms.py b/bdikit/mapping_algorithms/column_mapping/algorithms.py index f6788b36..1088a9f3 100644 --- a/bdikit/mapping_algorithms/column_mapping/algorithms.py +++ b/bdikit/mapping_algorithms/column_mapping/algorithms.py @@ -11,25 +11,36 @@ ) from valentine.algorithms.matcher_results import MatcherResults from openai import OpenAI +from bdikit.mapping_algorithms.scope_reducing._algorithms.contrastive_learning.cl_api import ( + ContrastiveLearningAPI +) +from bdikit.download import get_cached_model_or_download class BaseColumnMappingAlgorithm: def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame) -> Dict[str, str]: raise NotImplementedError("Subclasses must implement this method") + def _fill_missing_matches(self, dataset: pd.DataFrame, matches: Dict[str, str]) -> Dict[str, str]: + for column in dataset.columns: + if column not in matches: + matches[column] = "" + return matches + class ValentineColumnMappingAlgorithm(BaseColumnMappingAlgorithm): def __init__(self, matcher: BaseMatcher): self.matcher = matcher def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame) -> Dict[str, str]: - matches: MatcherResults = valentine_match(dataset, global_table, self.matcher) + matches: MatcherResults = valentine_match( + dataset, global_table, self.matcher) mappings = {} for match in matches.one_to_one(): dataset_candidate = match[0][1] global_table_candidate = match[1][1] mappings[dataset_candidate] = global_table_candidate - return mappings + return self._fill_missing_matches(dataset, mappings) class SimFloodAlgorithm(ValentineColumnMappingAlgorithm): @@ -73,14 +84,15 @@ def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame): rows = values.sample(15).tolist() else: rows = values.tolist() - serialized_input = f"{column}: {', '.join([str(row) for row in rows])}" + serialized_input = f"{column}: { + ', '.join([str(row) for row in rows])}" context = serialized_input.lower() column_types = self.get_column_type(context, labels) for column_type in column_types: if column_type in global_columns: mappings[column] = column_type break - return mappings + return self._fill_missing_matches(dataset, mappings) def get_column_type(self, context, labels, m=10, model="gpt-4-turbo-preview"): messages = [ @@ -102,3 +114,42 @@ def get_column_type(self, context, labels, m=10, model="gpt-4-turbo-preview"): ) col_type_content = col_type.choices[0].message.content return col_type_content.split(";") + + +class ContrastiveLearningAlgorithm(BaseColumnMappingAlgorithm): + def __init__(self): + model_path = get_cached_model_or_download("cl-reducer-v0.1") + self.api = ContrastiveLearningAPI(model_path=model_path, top_k=20) + + def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame): + union_scopes, scopes_json = self.api.get_recommendations(dataset) + matches = {} + for column, scope in zip(dataset.columns, scopes_json): + candidate = scope["Top k columns"][0][0] + if candidate in global_table.columns: # this check protects against the case where the candidate generated from the model is not in the global table + matches[column] = candidate + return self._fill_missing_matches(dataset, matches) + + +class TwoPhaseMatcherAlgorithm(BaseColumnMappingAlgorithm): + def __init__(self): + model_path = get_cached_model_or_download("cl-reducer-v0.1") + self.api = ContrastiveLearningAPI(model_path=model_path, top_k=20) + + def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame, algorithm: BaseColumnMappingAlgorithm = SimFloodAlgorithm()): + union_scopes, scopes_json = self.api.get_recommendations(dataset) + matches = {} + for column, scope in zip(dataset.columns, scopes_json): + candidates = [cand[0] for cand in scope["Top k columns"] + if cand[0] in global_table.columns] + reduced_dataset = dataset[[column]] + reduced_global_table = global_table[candidates] + partial_matches = algorithm.map( + reduced_dataset, reduced_global_table) + + if len(partial_matches.keys()) > 0: + candidate_col = next(iter(partial_matches)) + target_col = partial_matches[candidate_col] + matches[candidate_col] = target_col + + return self._fill_missing_matches(dataset, matches) diff --git a/examples/column_matching.ipynb b/examples/column_matching.ipynb new file mode 100644 index 00000000..79cfe39d --- /dev/null +++ b/examples/column_matching.ipynb @@ -0,0 +1,983 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b6a5938c-d32e-4816-ba31-a0b54e4a9826", + "metadata": {}, + "source": [ + "# Data Integration With `bdi-kit`" + ] + }, + { + "cell_type": "markdown", + "id": "1af374aa-f7b2-4329-b605-faf71c17e6b0", + "metadata": {}, + "source": [ + "First, import the class `APIManager`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f01f77a6-f349-4f71-a847-c26b69a60c41", + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n var force = true;\n var py_version = '3.4.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n var reloading = false;\n var Bokeh = root.Bokeh;\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n run_callbacks();\n return null;\n }\n if (!reloading) {\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error() {\n console.error(\"failed to load \" + url);\n }\n\n var skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {'tabulator': 'https://cdn.jsdelivr.net/npm/tabulator-tables@5.5.0/dist/js/tabulator.min', 'moment': 'https://cdn.jsdelivr.net/npm/luxon/build/global/luxon.min'}, 'shim': {}});\n require([\"tabulator\"], function(Tabulator) {\n\twindow.Tabulator = Tabulator\n\ton_load()\n })\n require([\"moment\"], function(moment) {\n\twindow.moment = moment\n\ton_load()\n })\n root._bokeh_is_loading = css_urls.length + 2;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n var existing_stylesheets = []\n var links = document.getElementsByTagName('link')\n for (var i = 0; i < links.length; i++) {\n var link = links[i]\n if (link.href != null) {\n\texisting_stylesheets.push(link.href)\n }\n }\n for (var i = 0; i < css_urls.length; i++) {\n var url = css_urls[i];\n if (existing_stylesheets.indexOf(url) !== -1) {\n\ton_load()\n\tcontinue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } if (((window.Tabulator !== undefined) && (!(window.Tabulator instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window.moment !== undefined) && (!(window.moment instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/luxon/build/global/luxon.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } var existing_scripts = []\n var scripts = document.getElementsByTagName('script')\n for (var i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n\texisting_scripts.push(script.src)\n }\n }\n for (var i = 0; i < js_urls.length; i++) {\n var url = js_urls[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (var i = 0; i < js_modules.length; i++) {\n var url = js_modules[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n var url = js_exports[name];\n if (skip.indexOf(url) >= 0 || root[name] != null) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n var js_urls = [\"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.min.js\", \"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/luxon/build/global/luxon.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.4.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.4.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.4.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.4.1.min.js\", \"https://cdn.holoviz.org/panel/1.4.4/dist/panel.min.js\"];\n var js_modules = [];\n var js_exports = {};\n var css_urls = [\"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_simple.min.css?v=1.4.4\"];\n var inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (var i = 0; i < inline_js.length; i++) {\n\ttry {\n inline_js[i].call(root, root.Bokeh);\n\t} catch(e) {\n\t if (!reloading) {\n\t throw e;\n\t }\n\t}\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n\tvar NewBokeh = root.Bokeh;\n\tif (Bokeh.versions === undefined) {\n\t Bokeh.versions = new Map();\n\t}\n\tif (NewBokeh.version !== Bokeh.version) {\n\t Bokeh.versions.set(NewBokeh.version, NewBokeh)\n\t}\n\troot.Bokeh = Bokeh;\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n\troot.Bokeh = undefined;\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n\tconsole.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n\trun_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n comm.open();\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n }) \n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ] + }, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "aa13de2e-3220-49d5-8e94-116b886b3f76" + } + }, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n var force = true;\n var py_version = '3.4.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n var reloading = true;\n var Bokeh = root.Bokeh;\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n run_callbacks();\n return null;\n }\n if (!reloading) {\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error() {\n console.error(\"failed to load \" + url);\n }\n\n var skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {'tabulator': 'https://cdn.jsdelivr.net/npm/tabulator-tables@5.5.0/dist/js/tabulator.min', 'moment': 'https://cdn.jsdelivr.net/npm/luxon/build/global/luxon.min', 'mathjax': '//cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS_HTML'}, 'shim': {'mathjax': {'exports': 'MathJax'}}});\n require([\"tabulator\"], function(Tabulator) {\n\twindow.Tabulator = Tabulator\n\ton_load()\n })\n require([\"moment\"], function(moment) {\n\twindow.moment = moment\n\ton_load()\n })\n require([\"mathjax\"], function() {\n\ton_load()\n })\n root._bokeh_is_loading = css_urls.length + 3;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n var existing_stylesheets = []\n var links = document.getElementsByTagName('link')\n for (var i = 0; i < links.length; i++) {\n var link = links[i]\n if (link.href != null) {\n\texisting_stylesheets.push(link.href)\n }\n }\n for (var i = 0; i < css_urls.length; i++) {\n var url = css_urls[i];\n if (existing_stylesheets.indexOf(url) !== -1) {\n\ton_load()\n\tcontinue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } if (((window.Tabulator !== undefined) && (!(window.Tabulator instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window.moment !== undefined) && (!(window.moment instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/luxon/build/global/luxon.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window.MathJax !== undefined) && (!(window.MathJax instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } var existing_scripts = []\n var scripts = document.getElementsByTagName('script')\n for (var i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n\texisting_scripts.push(script.src)\n }\n }\n for (var i = 0; i < js_urls.length; i++) {\n var url = js_urls[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (var i = 0; i < js_modules.length; i++) {\n var url = js_modules[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n var url = js_exports[name];\n if (skip.indexOf(url) >= 0 || root[name] != null) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n var js_urls = [\"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML\", \"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.min.js\", \"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/luxon/build/global/luxon.min.js\"];\n var js_modules = [];\n var js_exports = {};\n var css_urls = [\"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_simple.min.css?v=1.4.4\"];\n var inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (var i = 0; i < inline_js.length; i++) {\n\ttry {\n inline_js[i].call(root, root.Bokeh);\n\t} catch(e) {\n\t if (!reloading) {\n\t throw e;\n\t }\n\t}\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n\tvar NewBokeh = root.Bokeh;\n\tif (Bokeh.versions === undefined) {\n\t Bokeh.versions = new Map();\n\t}\n\tif (NewBokeh.version !== Bokeh.version) {\n\t Bokeh.versions.set(NewBokeh.version, NewBokeh)\n\t}\n\troot.Bokeh = Bokeh;\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n\troot.Bokeh = undefined;\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n\tconsole.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n\trun_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n comm.open();\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n }) \n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n var force = true;\n var py_version = '3.4.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n var reloading = true;\n var Bokeh = root.Bokeh;\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n run_callbacks();\n return null;\n }\n if (!reloading) {\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error() {\n console.error(\"failed to load \" + url);\n }\n\n var skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {'tabulator': 'https://cdn.jsdelivr.net/npm/tabulator-tables@5.5.0/dist/js/tabulator.min', 'moment': 'https://cdn.jsdelivr.net/npm/luxon/build/global/luxon.min', 'mathjax': '//cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS_HTML', 'vega-embed': 'https://cdn.jsdelivr.net/npm/vega-embed@6/build/vega-embed.min', 'vega-lite': 'https://cdn.jsdelivr.net/npm/vega-lite@5/build/vega-lite.min', 'vega': 'https://cdn.jsdelivr.net/npm/vega@5/build/vega.min'}, 'shim': {'mathjax': {'exports': 'MathJax'}}});\n require([\"tabulator\"], function(Tabulator) {\n\twindow.Tabulator = Tabulator\n\ton_load()\n })\n require([\"moment\"], function(moment) {\n\twindow.moment = moment\n\ton_load()\n })\n require([\"mathjax\"], function() {\n\ton_load()\n })\n require([\"vega-embed\"], function(vegaEmbed) {\n\twindow.vegaEmbed = vegaEmbed\n\ton_load()\n })\n require([\"vega-lite\"], function(vl) {\n\twindow.vl = vl\n\ton_load()\n })\n require([\"vega\"], function(vega) {\n\twindow.vega = vega\n\ton_load()\n })\n root._bokeh_is_loading = css_urls.length + 6;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n var existing_stylesheets = []\n var links = document.getElementsByTagName('link')\n for (var i = 0; i < links.length; i++) {\n var link = links[i]\n if (link.href != null) {\n\texisting_stylesheets.push(link.href)\n }\n }\n for (var i = 0; i < css_urls.length; i++) {\n var url = css_urls[i];\n if (existing_stylesheets.indexOf(url) !== -1) {\n\ton_load()\n\tcontinue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } if (((window.Tabulator !== undefined) && (!(window.Tabulator instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window.moment !== undefined) && (!(window.moment instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/luxon/build/global/luxon.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window.MathJax !== undefined) && (!(window.MathJax instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window.vega !== undefined) && (!(window.vega instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/vegaplot/vega@5'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window.vegaLite !== undefined) && (!(window.vegaLite instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/vegaplot/vega-lite@5'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window.vegaEmbed !== undefined) && (!(window.vegaEmbed instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/vegaplot/vega-embed@6'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } var existing_scripts = []\n var scripts = document.getElementsByTagName('script')\n for (var i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n\texisting_scripts.push(script.src)\n }\n }\n for (var i = 0; i < js_urls.length; i++) {\n var url = js_urls[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (var i = 0; i < js_modules.length; i++) {\n var url = js_modules[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n var url = js_exports[name];\n if (skip.indexOf(url) >= 0 || root[name] != null) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n var js_urls = [\"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML\", \"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.min.js\", \"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/luxon/build/global/luxon.min.js\", \"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/vegaplot/vega@5\", \"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/vegaplot/vega-lite@5\", \"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/vegaplot/vega-embed@6\"];\n var js_modules = [];\n var js_exports = {};\n var css_urls = [\"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_simple.min.css?v=1.4.4\"];\n var inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (var i = 0; i < inline_js.length; i++) {\n\ttry {\n inline_js[i].call(root, root.Bokeh);\n\t} catch(e) {\n\t if (!reloading) {\n\t throw e;\n\t }\n\t}\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n\tvar NewBokeh = root.Bokeh;\n\tif (Bokeh.versions === undefined) {\n\t Bokeh.versions = new Map();\n\t}\n\tif (NewBokeh.version !== Bokeh.version) {\n\t Bokeh.versions.set(NewBokeh.version, NewBokeh)\n\t}\n\troot.Bokeh = Bokeh;\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n\troot.Bokeh = undefined;\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n\tconsole.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n\trun_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n comm.open();\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n }) \n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import pandas as pd\n", + "import bdikit as bdi" + ] + }, + { + "cell_type": "markdown", + "id": "fb0b6170-c8e5-4ff3-9948-a55acb913563", + "metadata": {}, + "source": [ + "## Dataset Loading" + ] + }, + { + "cell_type": "markdown", + "id": "ee01a713-6cea-42a7-9a64-7aeaba7b0857", + "metadata": {}, + "source": [ + "In this example, we are mapping data from Dou et al. (https://pubmed.ncbi.nlm.nih.gov/37567170/) to the GDC format." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "99afdf9a-b68a-4a30-9735-f8ef36027757", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
CountryHistologic_Grade_FIGOHistologic_typePath_Stage_Primary_Tumor-pTPath_Stage_Reg_Lymph_Nodes-pNClin_Stage_Dist_Mets-cMPath_Stage_Dist_Mets-pMtumor_Stage-PathologicalFIGO_stageBMIAgeRaceEthnicityGenderTumor_SiteTumor_FocalityTumor_Size_cm
0United StatesFIGO grade 1EndometrioidpT1a (FIGO IA)pN0cM0Staging IncompleteStage IIA38.8864.0WhiteNot-Hispanic or LatinoFemaleAnterior endometriumUnifocal2.9
1United StatesFIGO grade 1EndometrioidpT1a (FIGO IA)pNXcM0Staging IncompleteStage IVIA39.7658.0WhiteNot-Hispanic or LatinoFemalePosterior endometriumUnifocal3.5
2United StatesFIGO grade 2EndometrioidpT1a (FIGO IA)pN0cM0Staging IncompleteStage IIA51.1950.0WhiteNot-Hispanic or LatinoFemaleOther, specifyUnifocal4.5
3NaNNaNCarcinosarcomaNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
4United StatesFIGO grade 2EndometrioidpT1a (FIGO IA)pNXcM0No pathologic evidence of distant metastasisStage IIA32.6975.0WhiteNot-Hispanic or LatinoFemaleOther, specifyUnifocal3.5
......................................................
99UkraineFIGO grade 3EndometrioidpT1a (FIGO IA)pNXcM0Staging IncompleteStage IIA29.4075.0NaNNaNFemaleOther, specifyUnifocal4.2
100UkraineFIGO grade 2EndometrioidpT2 (FIGO II)pN0cM0Staging IncompleteStage IIII35.4274.0NaNNaNFemaleOther, specifyUnifocal1.5
101United StatesNaNSerouspT2 (FIGO II)pN0Staging IncompleteStaging IncompleteStage IIII24.3285.0Black or African AmericanNot-Hispanic or LatinoFemaleOther, specifyUnifocal3.8
102UkraineNaNSerouspT1a (FIGO IA)pN0cM0Staging IncompleteStage IIA34.0670.0NaNNaNFemaleOther, specifyUnifocal5.0
103UkraineNaNSerousNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
\n", + "

104 rows × 17 columns

\n", + "
" + ], + "text/plain": [ + " Country Histologic_Grade_FIGO Histologic_type \\\n", + "0 United States FIGO grade 1 Endometrioid \n", + "1 United States FIGO grade 1 Endometrioid \n", + "2 United States FIGO grade 2 Endometrioid \n", + "3 NaN NaN Carcinosarcoma \n", + "4 United States FIGO grade 2 Endometrioid \n", + ".. ... ... ... \n", + "99 Ukraine FIGO grade 3 Endometrioid \n", + "100 Ukraine FIGO grade 2 Endometrioid \n", + "101 United States NaN Serous \n", + "102 Ukraine NaN Serous \n", + "103 Ukraine NaN Serous \n", + "\n", + " Path_Stage_Primary_Tumor-pT Path_Stage_Reg_Lymph_Nodes-pN \\\n", + "0 pT1a (FIGO IA) pN0 \n", + "1 pT1a (FIGO IA) pNX \n", + "2 pT1a (FIGO IA) pN0 \n", + "3 NaN NaN \n", + "4 pT1a (FIGO IA) pNX \n", + ".. ... ... \n", + "99 pT1a (FIGO IA) pNX \n", + "100 pT2 (FIGO II) pN0 \n", + "101 pT2 (FIGO II) pN0 \n", + "102 pT1a (FIGO IA) pN0 \n", + "103 NaN NaN \n", + "\n", + " Clin_Stage_Dist_Mets-cM Path_Stage_Dist_Mets-pM \\\n", + "0 cM0 Staging Incomplete \n", + "1 cM0 Staging Incomplete \n", + "2 cM0 Staging Incomplete \n", + "3 NaN NaN \n", + "4 cM0 No pathologic evidence of distant metastasis \n", + ".. ... ... \n", + "99 cM0 Staging Incomplete \n", + "100 cM0 Staging Incomplete \n", + "101 Staging Incomplete Staging Incomplete \n", + "102 cM0 Staging Incomplete \n", + "103 NaN NaN \n", + "\n", + " tumor_Stage-Pathological FIGO_stage BMI Age \\\n", + "0 Stage I IA 38.88 64.0 \n", + "1 Stage IV IA 39.76 58.0 \n", + "2 Stage I IA 51.19 50.0 \n", + "3 NaN NaN NaN NaN \n", + "4 Stage I IA 32.69 75.0 \n", + ".. ... ... ... ... \n", + "99 Stage I IA 29.40 75.0 \n", + "100 Stage II II 35.42 74.0 \n", + "101 Stage II II 24.32 85.0 \n", + "102 Stage I IA 34.06 70.0 \n", + "103 NaN NaN NaN NaN \n", + "\n", + " Race Ethnicity Gender \\\n", + "0 White Not-Hispanic or Latino Female \n", + "1 White Not-Hispanic or Latino Female \n", + "2 White Not-Hispanic or Latino Female \n", + "3 NaN NaN NaN \n", + "4 White Not-Hispanic or Latino Female \n", + ".. ... ... ... \n", + "99 NaN NaN Female \n", + "100 NaN NaN Female \n", + "101 Black or African American Not-Hispanic or Latino Female \n", + "102 NaN NaN Female \n", + "103 NaN NaN NaN \n", + "\n", + " Tumor_Site Tumor_Focality Tumor_Size_cm \n", + "0 Anterior endometrium Unifocal 2.9 \n", + "1 Posterior endometrium Unifocal 3.5 \n", + "2 Other, specify Unifocal 4.5 \n", + "3 NaN NaN NaN \n", + "4 Other, specify Unifocal 3.5 \n", + ".. ... ... ... \n", + "99 Other, specify Unifocal 4.2 \n", + "100 Other, specify Unifocal 1.5 \n", + "101 Other, specify Unifocal 3.8 \n", + "102 Other, specify Unifocal 5.0 \n", + "103 NaN NaN NaN \n", + "\n", + "[104 rows x 17 columns]" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset_path = './datasets/dou.csv'\n", + "dataset = pd.read_csv(dataset_path)\n", + "dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1a823ad7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcetarget
0tumor_Stage-Pathologicalensat_pathologic_stage
1Ethnicityethnicity
2Tumor_Focalitytumor_focality
3Racerace
4Tumor_Size_cmbreslow_thickness
5Gendergender
6Ageamount
7Country
8Histologic_Grade_FIGO
9Histologic_type
10Path_Stage_Primary_Tumor-pT
11Path_Stage_Reg_Lymph_Nodes-pN
12Clin_Stage_Dist_Mets-cM
13Path_Stage_Dist_Mets-pM
14FIGO_stage
15BMI
16Tumor_Site
\n", + "
" + ], + "text/plain": [ + " source target\n", + "0 tumor_Stage-Pathological ensat_pathologic_stage\n", + "1 Ethnicity ethnicity\n", + "2 Tumor_Focality tumor_focality\n", + "3 Race race\n", + "4 Tumor_Size_cm breslow_thickness\n", + "5 Gender gender\n", + "6 Age amount\n", + "7 Country \n", + "8 Histologic_Grade_FIGO \n", + "9 Histologic_type \n", + "10 Path_Stage_Primary_Tumor-pT \n", + "11 Path_Stage_Reg_Lymph_Nodes-pN \n", + "12 Clin_Stage_Dist_Mets-cM \n", + "13 Path_Stage_Dist_Mets-pM \n", + "14 FIGO_stage \n", + "15 BMI \n", + "16 Tumor_Site " + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "matches = bdi.match_columns(dataset, target=\"gdc\", method= \"similarity_flooding\")\n", + "matches" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "68771c9e", + "metadata": {}, + "outputs": [], + "source": [ + "# matches = bdi.match_columns(dataset, target=\"gdc\", method= \"ct_learning\")\n", + "# matches" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "564abc0a", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Some weights of RobertaModel were not initialized from the model checkpoint at roberta-base and are newly initialized: ['roberta.pooler.dense.bias', 'roberta.pooler.dense.weight']\n", + "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n", + " 0%| | 0/17 [00:00\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcetarget
0Countrycountry_of_birth
1Histologic_Grade_FIGOhistologic_progression_type
2Histologic_typedysplasia_type
3Path_Stage_Primary_Tumor-pTuicc_clinical_m
4Path_Stage_Reg_Lymph_Nodes-pNfigo_stage
5Clin_Stage_Dist_Mets-cMinrg_stage
6Path_Stage_Dist_Mets-pMlast_known_disease_status
7tumor_Stage-Pathologicaltumor_grade_category
8FIGO_stagefigo_stage
9BMIhpv_positive_type
10Ageweight
11Racerace
12Ethnicityethnicity
13Gendergender
14Tumor_Sitetumor_shape
15Tumor_Focalitytumor_focality
16Tumor_Size_cmtumor_depth
\n", + "" + ], + "text/plain": [ + " source target\n", + "0 Country country_of_birth\n", + "1 Histologic_Grade_FIGO histologic_progression_type\n", + "2 Histologic_type dysplasia_type\n", + "3 Path_Stage_Primary_Tumor-pT uicc_clinical_m\n", + "4 Path_Stage_Reg_Lymph_Nodes-pN figo_stage\n", + "5 Clin_Stage_Dist_Mets-cM inrg_stage\n", + "6 Path_Stage_Dist_Mets-pM last_known_disease_status\n", + "7 tumor_Stage-Pathological tumor_grade_category\n", + "8 FIGO_stage figo_stage\n", + "9 BMI hpv_positive_type\n", + "10 Age weight\n", + "11 Race race\n", + "12 Ethnicity ethnicity\n", + "13 Gender gender\n", + "14 Tumor_Site tumor_shape\n", + "15 Tumor_Focality tumor_focality\n", + "16 Tumor_Size_cm tumor_depth" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "matches = bdi.match_columns(dataset, target=\"gdc\", method= \"two_phase\")\n", + "matches" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From ed4d42c1d99b68f74b92a1d884e1c95663745136 Mon Sep 17 00:00:00 2001 From: EduardoPena Date: Mon, 10 Jun 2024 17:31:14 -0400 Subject: [PATCH 06/22] Formating with black, small fix for columns with no matches --- bdikit/functional_api.py | 2 +- .../column_mapping/algorithms.py | 30 ++++++++++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/bdikit/functional_api.py b/bdikit/functional_api.py index 48fcaa6a..c5461ac2 100644 --- a/bdikit/functional_api.py +++ b/bdikit/functional_api.py @@ -12,7 +12,7 @@ JaccardDistanceAlgorithm, GPTAlgorithm, ContrastiveLearningAlgorithm, - TwoPhaseMatcherAlgorithm + TwoPhaseMatcherAlgorithm, ) from bdikit.mapping_algorithms.value_mapping.value_mappers import ValueMapper from bdikit.mapping_algorithms.scope_reducing._algorithms.contrastive_learning.cl_api import ( diff --git a/bdikit/mapping_algorithms/column_mapping/algorithms.py b/bdikit/mapping_algorithms/column_mapping/algorithms.py index 1088a9f3..4512b65e 100644 --- a/bdikit/mapping_algorithms/column_mapping/algorithms.py +++ b/bdikit/mapping_algorithms/column_mapping/algorithms.py @@ -12,7 +12,7 @@ from valentine.algorithms.matcher_results import MatcherResults from openai import OpenAI from bdikit.mapping_algorithms.scope_reducing._algorithms.contrastive_learning.cl_api import ( - ContrastiveLearningAPI + ContrastiveLearningAPI, ) from bdikit.download import get_cached_model_or_download @@ -21,7 +21,9 @@ class BaseColumnMappingAlgorithm: def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame) -> Dict[str, str]: raise NotImplementedError("Subclasses must implement this method") - def _fill_missing_matches(self, dataset: pd.DataFrame, matches: Dict[str, str]) -> Dict[str, str]: + def _fill_missing_matches( + self, dataset: pd.DataFrame, matches: Dict[str, str] + ) -> Dict[str, str]: for column in dataset.columns: if column not in matches: matches[column] = "" @@ -33,8 +35,7 @@ def __init__(self, matcher: BaseMatcher): self.matcher = matcher def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame) -> Dict[str, str]: - matches: MatcherResults = valentine_match( - dataset, global_table, self.matcher) + matches: MatcherResults = valentine_match(dataset, global_table, self.matcher) mappings = {} for match in matches.one_to_one(): dataset_candidate = match[0][1] @@ -126,7 +127,9 @@ def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame): matches = {} for column, scope in zip(dataset.columns, scopes_json): candidate = scope["Top k columns"][0][0] - if candidate in global_table.columns: # this check protects against the case where the candidate generated from the model is not in the global table + if ( + candidate in global_table.columns + ): # this check protects against the case where the candidate generated from the model is not in the global table matches[column] = candidate return self._fill_missing_matches(dataset, matches) @@ -136,16 +139,23 @@ def __init__(self): model_path = get_cached_model_or_download("cl-reducer-v0.1") self.api = ContrastiveLearningAPI(model_path=model_path, top_k=20) - def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame, algorithm: BaseColumnMappingAlgorithm = SimFloodAlgorithm()): + def map( + self, + dataset: pd.DataFrame, + global_table: pd.DataFrame, + algorithm: BaseColumnMappingAlgorithm = SimFloodAlgorithm(), + ): union_scopes, scopes_json = self.api.get_recommendations(dataset) matches = {} for column, scope in zip(dataset.columns, scopes_json): - candidates = [cand[0] for cand in scope["Top k columns"] - if cand[0] in global_table.columns] + candidates = [ + cand[0] + for cand in scope["Top k columns"] + if cand[0] in global_table.columns + ] reduced_dataset = dataset[[column]] reduced_global_table = global_table[candidates] - partial_matches = algorithm.map( - reduced_dataset, reduced_global_table) + partial_matches = algorithm.map(reduced_dataset, reduced_global_table) if len(partial_matches.keys()) > 0: candidate_col = next(iter(partial_matches)) From 5e1f147ef14e06150e4b322a7df9f6d47cb5abdf Mon Sep 17 00:00:00 2001 From: EduardoPena Date: Mon, 10 Jun 2024 17:50:38 -0400 Subject: [PATCH 07/22] Formating using black variation --- bdikit/mapping_algorithms/column_mapping/algorithms.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bdikit/mapping_algorithms/column_mapping/algorithms.py b/bdikit/mapping_algorithms/column_mapping/algorithms.py index 4512b65e..e85e956d 100644 --- a/bdikit/mapping_algorithms/column_mapping/algorithms.py +++ b/bdikit/mapping_algorithms/column_mapping/algorithms.py @@ -85,8 +85,7 @@ def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame): rows = values.sample(15).tolist() else: rows = values.tolist() - serialized_input = f"{column}: { - ', '.join([str(row) for row in rows])}" + serialized_input = f"{column}: {', '.join([str(row) for row in rows])}" context = serialized_input.lower() column_types = self.get_column_type(context, labels) for column_type in column_types: From c85de2be7e570f9b6c45005fd7603e77538cd1a2 Mon Sep 17 00:00:00 2001 From: EduardoPena Date: Tue, 11 Jun 2024 15:17:14 -0400 Subject: [PATCH 08/22] Moving model_name,top_k to construtor parameters for better class reuse --- .../column_mapping/algorithms.py | 16 +- examples/column_matching.ipynb | 983 ------------------ 2 files changed, 7 insertions(+), 992 deletions(-) delete mode 100644 examples/column_matching.ipynb diff --git a/bdikit/mapping_algorithms/column_mapping/algorithms.py b/bdikit/mapping_algorithms/column_mapping/algorithms.py index e85e956d..697cd8d7 100644 --- a/bdikit/mapping_algorithms/column_mapping/algorithms.py +++ b/bdikit/mapping_algorithms/column_mapping/algorithms.py @@ -117,26 +117,24 @@ def get_column_type(self, context, labels, m=10, model="gpt-4-turbo-preview"): class ContrastiveLearningAlgorithm(BaseColumnMappingAlgorithm): - def __init__(self): - model_path = get_cached_model_or_download("cl-reducer-v0.1") - self.api = ContrastiveLearningAPI(model_path=model_path, top_k=20) + def __init__(self, model_name: str = "cl-reducer-v0.1"): + model_path = get_cached_model_or_download(model_name) + self.api = ContrastiveLearningAPI(model_path=model_path, top_k=1) def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame): union_scopes, scopes_json = self.api.get_recommendations(dataset) matches = {} for column, scope in zip(dataset.columns, scopes_json): candidate = scope["Top k columns"][0][0] - if ( - candidate in global_table.columns - ): # this check protects against the case where the candidate generated from the model is not in the global table + if candidate in global_table.columns: matches[column] = candidate return self._fill_missing_matches(dataset, matches) class TwoPhaseMatcherAlgorithm(BaseColumnMappingAlgorithm): - def __init__(self): - model_path = get_cached_model_or_download("cl-reducer-v0.1") - self.api = ContrastiveLearningAPI(model_path=model_path, top_k=20) + def __init__(self, model_name: str = "cl-reducer-v0.1", top_k: int = 20): + model_path = get_cached_model_or_download(model_name) + self.api = ContrastiveLearningAPI(model_path=model_path, top_k=top_k) def map( self, diff --git a/examples/column_matching.ipynb b/examples/column_matching.ipynb deleted file mode 100644 index 79cfe39d..00000000 --- a/examples/column_matching.ipynb +++ /dev/null @@ -1,983 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "b6a5938c-d32e-4816-ba31-a0b54e4a9826", - "metadata": {}, - "source": [ - "# Data Integration With `bdi-kit`" - ] - }, - { - "cell_type": "markdown", - "id": "1af374aa-f7b2-4329-b605-faf71c17e6b0", - "metadata": {}, - "source": [ - "First, import the class `APIManager`." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "f01f77a6-f349-4f71-a847-c26b69a60c41", - "metadata": {}, - "outputs": [ - { - "data": { - "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n var force = true;\n var py_version = '3.4.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n var reloading = false;\n var Bokeh = root.Bokeh;\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n run_callbacks();\n return null;\n }\n if (!reloading) {\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error() {\n console.error(\"failed to load \" + url);\n }\n\n var skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {'tabulator': 'https://cdn.jsdelivr.net/npm/tabulator-tables@5.5.0/dist/js/tabulator.min', 'moment': 'https://cdn.jsdelivr.net/npm/luxon/build/global/luxon.min'}, 'shim': {}});\n require([\"tabulator\"], function(Tabulator) {\n\twindow.Tabulator = Tabulator\n\ton_load()\n })\n require([\"moment\"], function(moment) {\n\twindow.moment = moment\n\ton_load()\n })\n root._bokeh_is_loading = css_urls.length + 2;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n var existing_stylesheets = []\n var links = document.getElementsByTagName('link')\n for (var i = 0; i < links.length; i++) {\n var link = links[i]\n if (link.href != null) {\n\texisting_stylesheets.push(link.href)\n }\n }\n for (var i = 0; i < css_urls.length; i++) {\n var url = css_urls[i];\n if (existing_stylesheets.indexOf(url) !== -1) {\n\ton_load()\n\tcontinue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } if (((window.Tabulator !== undefined) && (!(window.Tabulator instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window.moment !== undefined) && (!(window.moment instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/luxon/build/global/luxon.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } var existing_scripts = []\n var scripts = document.getElementsByTagName('script')\n for (var i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n\texisting_scripts.push(script.src)\n }\n }\n for (var i = 0; i < js_urls.length; i++) {\n var url = js_urls[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (var i = 0; i < js_modules.length; i++) {\n var url = js_modules[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n var url = js_exports[name];\n if (skip.indexOf(url) >= 0 || root[name] != null) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n var js_urls = [\"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.min.js\", \"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/luxon/build/global/luxon.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.4.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.4.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.4.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.4.1.min.js\", \"https://cdn.holoviz.org/panel/1.4.4/dist/panel.min.js\"];\n var js_modules = [];\n var js_exports = {};\n var css_urls = [\"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_simple.min.css?v=1.4.4\"];\n var inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (var i = 0; i < inline_js.length; i++) {\n\ttry {\n inline_js[i].call(root, root.Bokeh);\n\t} catch(e) {\n\t if (!reloading) {\n\t throw e;\n\t }\n\t}\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n\tvar NewBokeh = root.Bokeh;\n\tif (Bokeh.versions === undefined) {\n\t Bokeh.versions = new Map();\n\t}\n\tif (NewBokeh.version !== Bokeh.version) {\n\t Bokeh.versions.set(NewBokeh.version, NewBokeh)\n\t}\n\troot.Bokeh = Bokeh;\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n\troot.Bokeh = undefined;\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n\tconsole.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n\trun_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n comm.open();\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n }) \n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "aa13de2e-3220-49d5-8e94-116b886b3f76" - } - }, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n var force = true;\n var py_version = '3.4.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n var reloading = true;\n var Bokeh = root.Bokeh;\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n run_callbacks();\n return null;\n }\n if (!reloading) {\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error() {\n console.error(\"failed to load \" + url);\n }\n\n var skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {'tabulator': 'https://cdn.jsdelivr.net/npm/tabulator-tables@5.5.0/dist/js/tabulator.min', 'moment': 'https://cdn.jsdelivr.net/npm/luxon/build/global/luxon.min', 'mathjax': '//cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS_HTML'}, 'shim': {'mathjax': {'exports': 'MathJax'}}});\n require([\"tabulator\"], function(Tabulator) {\n\twindow.Tabulator = Tabulator\n\ton_load()\n })\n require([\"moment\"], function(moment) {\n\twindow.moment = moment\n\ton_load()\n })\n require([\"mathjax\"], function() {\n\ton_load()\n })\n root._bokeh_is_loading = css_urls.length + 3;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n var existing_stylesheets = []\n var links = document.getElementsByTagName('link')\n for (var i = 0; i < links.length; i++) {\n var link = links[i]\n if (link.href != null) {\n\texisting_stylesheets.push(link.href)\n }\n }\n for (var i = 0; i < css_urls.length; i++) {\n var url = css_urls[i];\n if (existing_stylesheets.indexOf(url) !== -1) {\n\ton_load()\n\tcontinue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } if (((window.Tabulator !== undefined) && (!(window.Tabulator instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window.moment !== undefined) && (!(window.moment instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/luxon/build/global/luxon.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window.MathJax !== undefined) && (!(window.MathJax instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } var existing_scripts = []\n var scripts = document.getElementsByTagName('script')\n for (var i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n\texisting_scripts.push(script.src)\n }\n }\n for (var i = 0; i < js_urls.length; i++) {\n var url = js_urls[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (var i = 0; i < js_modules.length; i++) {\n var url = js_modules[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n var url = js_exports[name];\n if (skip.indexOf(url) >= 0 || root[name] != null) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n var js_urls = [\"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML\", \"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.min.js\", \"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/luxon/build/global/luxon.min.js\"];\n var js_modules = [];\n var js_exports = {};\n var css_urls = [\"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_simple.min.css?v=1.4.4\"];\n var inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (var i = 0; i < inline_js.length; i++) {\n\ttry {\n inline_js[i].call(root, root.Bokeh);\n\t} catch(e) {\n\t if (!reloading) {\n\t throw e;\n\t }\n\t}\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n\tvar NewBokeh = root.Bokeh;\n\tif (Bokeh.versions === undefined) {\n\t Bokeh.versions = new Map();\n\t}\n\tif (NewBokeh.version !== Bokeh.version) {\n\t Bokeh.versions.set(NewBokeh.version, NewBokeh)\n\t}\n\troot.Bokeh = Bokeh;\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n\troot.Bokeh = undefined;\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n\tconsole.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n\trun_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n comm.open();\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n }) \n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n var force = true;\n var py_version = '3.4.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n var reloading = true;\n var Bokeh = root.Bokeh;\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n run_callbacks();\n return null;\n }\n if (!reloading) {\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error() {\n console.error(\"failed to load \" + url);\n }\n\n var skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {'tabulator': 'https://cdn.jsdelivr.net/npm/tabulator-tables@5.5.0/dist/js/tabulator.min', 'moment': 'https://cdn.jsdelivr.net/npm/luxon/build/global/luxon.min', 'mathjax': '//cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS_HTML', 'vega-embed': 'https://cdn.jsdelivr.net/npm/vega-embed@6/build/vega-embed.min', 'vega-lite': 'https://cdn.jsdelivr.net/npm/vega-lite@5/build/vega-lite.min', 'vega': 'https://cdn.jsdelivr.net/npm/vega@5/build/vega.min'}, 'shim': {'mathjax': {'exports': 'MathJax'}}});\n require([\"tabulator\"], function(Tabulator) {\n\twindow.Tabulator = Tabulator\n\ton_load()\n })\n require([\"moment\"], function(moment) {\n\twindow.moment = moment\n\ton_load()\n })\n require([\"mathjax\"], function() {\n\ton_load()\n })\n require([\"vega-embed\"], function(vegaEmbed) {\n\twindow.vegaEmbed = vegaEmbed\n\ton_load()\n })\n require([\"vega-lite\"], function(vl) {\n\twindow.vl = vl\n\ton_load()\n })\n require([\"vega\"], function(vega) {\n\twindow.vega = vega\n\ton_load()\n })\n root._bokeh_is_loading = css_urls.length + 6;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n var existing_stylesheets = []\n var links = document.getElementsByTagName('link')\n for (var i = 0; i < links.length; i++) {\n var link = links[i]\n if (link.href != null) {\n\texisting_stylesheets.push(link.href)\n }\n }\n for (var i = 0; i < css_urls.length; i++) {\n var url = css_urls[i];\n if (existing_stylesheets.indexOf(url) !== -1) {\n\ton_load()\n\tcontinue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } if (((window.Tabulator !== undefined) && (!(window.Tabulator instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window.moment !== undefined) && (!(window.moment instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/luxon/build/global/luxon.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window.MathJax !== undefined) && (!(window.MathJax instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window.vega !== undefined) && (!(window.vega instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/vegaplot/vega@5'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window.vegaLite !== undefined) && (!(window.vegaLite instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/vegaplot/vega-lite@5'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window.vegaEmbed !== undefined) && (!(window.vegaEmbed instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.4.4/dist/bundled/vegaplot/vega-embed@6'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } var existing_scripts = []\n var scripts = document.getElementsByTagName('script')\n for (var i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n\texisting_scripts.push(script.src)\n }\n }\n for (var i = 0; i < js_urls.length; i++) {\n var url = js_urls[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (var i = 0; i < js_modules.length; i++) {\n var url = js_modules[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n var url = js_exports[name];\n if (skip.indexOf(url) >= 0 || root[name] != null) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n var js_urls = [\"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML\", \"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.min.js\", \"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/luxon/build/global/luxon.min.js\", \"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/vegaplot/vega@5\", \"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/vegaplot/vega-lite@5\", \"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/vegaplot/vega-embed@6\"];\n var js_modules = [];\n var js_exports = {};\n var css_urls = [\"https://cdn.holoviz.org/panel/1.4.4/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_simple.min.css?v=1.4.4\"];\n var inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (var i = 0; i < inline_js.length; i++) {\n\ttry {\n inline_js[i].call(root, root.Bokeh);\n\t} catch(e) {\n\t if (!reloading) {\n\t throw e;\n\t }\n\t}\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n\tvar NewBokeh = root.Bokeh;\n\tif (Bokeh.versions === undefined) {\n\t Bokeh.versions = new Map();\n\t}\n\tif (NewBokeh.version !== Bokeh.version) {\n\t Bokeh.versions.set(NewBokeh.version, NewBokeh)\n\t}\n\troot.Bokeh = Bokeh;\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n\troot.Bokeh = undefined;\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n\tconsole.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n\trun_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n comm.open();\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n }) \n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import pandas as pd\n", - "import bdikit as bdi" - ] - }, - { - "cell_type": "markdown", - "id": "fb0b6170-c8e5-4ff3-9948-a55acb913563", - "metadata": {}, - "source": [ - "## Dataset Loading" - ] - }, - { - "cell_type": "markdown", - "id": "ee01a713-6cea-42a7-9a64-7aeaba7b0857", - "metadata": {}, - "source": [ - "In this example, we are mapping data from Dou et al. (https://pubmed.ncbi.nlm.nih.gov/37567170/) to the GDC format." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "99afdf9a-b68a-4a30-9735-f8ef36027757", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
CountryHistologic_Grade_FIGOHistologic_typePath_Stage_Primary_Tumor-pTPath_Stage_Reg_Lymph_Nodes-pNClin_Stage_Dist_Mets-cMPath_Stage_Dist_Mets-pMtumor_Stage-PathologicalFIGO_stageBMIAgeRaceEthnicityGenderTumor_SiteTumor_FocalityTumor_Size_cm
0United StatesFIGO grade 1EndometrioidpT1a (FIGO IA)pN0cM0Staging IncompleteStage IIA38.8864.0WhiteNot-Hispanic or LatinoFemaleAnterior endometriumUnifocal2.9
1United StatesFIGO grade 1EndometrioidpT1a (FIGO IA)pNXcM0Staging IncompleteStage IVIA39.7658.0WhiteNot-Hispanic or LatinoFemalePosterior endometriumUnifocal3.5
2United StatesFIGO grade 2EndometrioidpT1a (FIGO IA)pN0cM0Staging IncompleteStage IIA51.1950.0WhiteNot-Hispanic or LatinoFemaleOther, specifyUnifocal4.5
3NaNNaNCarcinosarcomaNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
4United StatesFIGO grade 2EndometrioidpT1a (FIGO IA)pNXcM0No pathologic evidence of distant metastasisStage IIA32.6975.0WhiteNot-Hispanic or LatinoFemaleOther, specifyUnifocal3.5
......................................................
99UkraineFIGO grade 3EndometrioidpT1a (FIGO IA)pNXcM0Staging IncompleteStage IIA29.4075.0NaNNaNFemaleOther, specifyUnifocal4.2
100UkraineFIGO grade 2EndometrioidpT2 (FIGO II)pN0cM0Staging IncompleteStage IIII35.4274.0NaNNaNFemaleOther, specifyUnifocal1.5
101United StatesNaNSerouspT2 (FIGO II)pN0Staging IncompleteStaging IncompleteStage IIII24.3285.0Black or African AmericanNot-Hispanic or LatinoFemaleOther, specifyUnifocal3.8
102UkraineNaNSerouspT1a (FIGO IA)pN0cM0Staging IncompleteStage IIA34.0670.0NaNNaNFemaleOther, specifyUnifocal5.0
103UkraineNaNSerousNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
\n", - "

104 rows × 17 columns

\n", - "
" - ], - "text/plain": [ - " Country Histologic_Grade_FIGO Histologic_type \\\n", - "0 United States FIGO grade 1 Endometrioid \n", - "1 United States FIGO grade 1 Endometrioid \n", - "2 United States FIGO grade 2 Endometrioid \n", - "3 NaN NaN Carcinosarcoma \n", - "4 United States FIGO grade 2 Endometrioid \n", - ".. ... ... ... \n", - "99 Ukraine FIGO grade 3 Endometrioid \n", - "100 Ukraine FIGO grade 2 Endometrioid \n", - "101 United States NaN Serous \n", - "102 Ukraine NaN Serous \n", - "103 Ukraine NaN Serous \n", - "\n", - " Path_Stage_Primary_Tumor-pT Path_Stage_Reg_Lymph_Nodes-pN \\\n", - "0 pT1a (FIGO IA) pN0 \n", - "1 pT1a (FIGO IA) pNX \n", - "2 pT1a (FIGO IA) pN0 \n", - "3 NaN NaN \n", - "4 pT1a (FIGO IA) pNX \n", - ".. ... ... \n", - "99 pT1a (FIGO IA) pNX \n", - "100 pT2 (FIGO II) pN0 \n", - "101 pT2 (FIGO II) pN0 \n", - "102 pT1a (FIGO IA) pN0 \n", - "103 NaN NaN \n", - "\n", - " Clin_Stage_Dist_Mets-cM Path_Stage_Dist_Mets-pM \\\n", - "0 cM0 Staging Incomplete \n", - "1 cM0 Staging Incomplete \n", - "2 cM0 Staging Incomplete \n", - "3 NaN NaN \n", - "4 cM0 No pathologic evidence of distant metastasis \n", - ".. ... ... \n", - "99 cM0 Staging Incomplete \n", - "100 cM0 Staging Incomplete \n", - "101 Staging Incomplete Staging Incomplete \n", - "102 cM0 Staging Incomplete \n", - "103 NaN NaN \n", - "\n", - " tumor_Stage-Pathological FIGO_stage BMI Age \\\n", - "0 Stage I IA 38.88 64.0 \n", - "1 Stage IV IA 39.76 58.0 \n", - "2 Stage I IA 51.19 50.0 \n", - "3 NaN NaN NaN NaN \n", - "4 Stage I IA 32.69 75.0 \n", - ".. ... ... ... ... \n", - "99 Stage I IA 29.40 75.0 \n", - "100 Stage II II 35.42 74.0 \n", - "101 Stage II II 24.32 85.0 \n", - "102 Stage I IA 34.06 70.0 \n", - "103 NaN NaN NaN NaN \n", - "\n", - " Race Ethnicity Gender \\\n", - "0 White Not-Hispanic or Latino Female \n", - "1 White Not-Hispanic or Latino Female \n", - "2 White Not-Hispanic or Latino Female \n", - "3 NaN NaN NaN \n", - "4 White Not-Hispanic or Latino Female \n", - ".. ... ... ... \n", - "99 NaN NaN Female \n", - "100 NaN NaN Female \n", - "101 Black or African American Not-Hispanic or Latino Female \n", - "102 NaN NaN Female \n", - "103 NaN NaN NaN \n", - "\n", - " Tumor_Site Tumor_Focality Tumor_Size_cm \n", - "0 Anterior endometrium Unifocal 2.9 \n", - "1 Posterior endometrium Unifocal 3.5 \n", - "2 Other, specify Unifocal 4.5 \n", - "3 NaN NaN NaN \n", - "4 Other, specify Unifocal 3.5 \n", - ".. ... ... ... \n", - "99 Other, specify Unifocal 4.2 \n", - "100 Other, specify Unifocal 1.5 \n", - "101 Other, specify Unifocal 3.8 \n", - "102 Other, specify Unifocal 5.0 \n", - "103 NaN NaN NaN \n", - "\n", - "[104 rows x 17 columns]" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "dataset_path = './datasets/dou.csv'\n", - "dataset = pd.read_csv(dataset_path)\n", - "dataset" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "1a823ad7", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
sourcetarget
0tumor_Stage-Pathologicalensat_pathologic_stage
1Ethnicityethnicity
2Tumor_Focalitytumor_focality
3Racerace
4Tumor_Size_cmbreslow_thickness
5Gendergender
6Ageamount
7Country
8Histologic_Grade_FIGO
9Histologic_type
10Path_Stage_Primary_Tumor-pT
11Path_Stage_Reg_Lymph_Nodes-pN
12Clin_Stage_Dist_Mets-cM
13Path_Stage_Dist_Mets-pM
14FIGO_stage
15BMI
16Tumor_Site
\n", - "
" - ], - "text/plain": [ - " source target\n", - "0 tumor_Stage-Pathological ensat_pathologic_stage\n", - "1 Ethnicity ethnicity\n", - "2 Tumor_Focality tumor_focality\n", - "3 Race race\n", - "4 Tumor_Size_cm breslow_thickness\n", - "5 Gender gender\n", - "6 Age amount\n", - "7 Country \n", - "8 Histologic_Grade_FIGO \n", - "9 Histologic_type \n", - "10 Path_Stage_Primary_Tumor-pT \n", - "11 Path_Stage_Reg_Lymph_Nodes-pN \n", - "12 Clin_Stage_Dist_Mets-cM \n", - "13 Path_Stage_Dist_Mets-pM \n", - "14 FIGO_stage \n", - "15 BMI \n", - "16 Tumor_Site " - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "matches = bdi.match_columns(dataset, target=\"gdc\", method= \"similarity_flooding\")\n", - "matches" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "68771c9e", - "metadata": {}, - "outputs": [], - "source": [ - "# matches = bdi.match_columns(dataset, target=\"gdc\", method= \"ct_learning\")\n", - "# matches" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "564abc0a", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Some weights of RobertaModel were not initialized from the model checkpoint at roberta-base and are newly initialized: ['roberta.pooler.dense.bias', 'roberta.pooler.dense.weight']\n", - "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n", - " 0%| | 0/17 [00:00\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
sourcetarget
0Countrycountry_of_birth
1Histologic_Grade_FIGOhistologic_progression_type
2Histologic_typedysplasia_type
3Path_Stage_Primary_Tumor-pTuicc_clinical_m
4Path_Stage_Reg_Lymph_Nodes-pNfigo_stage
5Clin_Stage_Dist_Mets-cMinrg_stage
6Path_Stage_Dist_Mets-pMlast_known_disease_status
7tumor_Stage-Pathologicaltumor_grade_category
8FIGO_stagefigo_stage
9BMIhpv_positive_type
10Ageweight
11Racerace
12Ethnicityethnicity
13Gendergender
14Tumor_Sitetumor_shape
15Tumor_Focalitytumor_focality
16Tumor_Size_cmtumor_depth
\n", - "" - ], - "text/plain": [ - " source target\n", - "0 Country country_of_birth\n", - "1 Histologic_Grade_FIGO histologic_progression_type\n", - "2 Histologic_type dysplasia_type\n", - "3 Path_Stage_Primary_Tumor-pT uicc_clinical_m\n", - "4 Path_Stage_Reg_Lymph_Nodes-pN figo_stage\n", - "5 Clin_Stage_Dist_Mets-cM inrg_stage\n", - "6 Path_Stage_Dist_Mets-pM last_known_disease_status\n", - "7 tumor_Stage-Pathological tumor_grade_category\n", - "8 FIGO_stage figo_stage\n", - "9 BMI hpv_positive_type\n", - "10 Age weight\n", - "11 Race race\n", - "12 Ethnicity ethnicity\n", - "13 Gender gender\n", - "14 Tumor_Site tumor_shape\n", - "15 Tumor_Focality tumor_focality\n", - "16 Tumor_Size_cm tumor_depth" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "matches = bdi.match_columns(dataset, target=\"gdc\", method= \"two_phase\")\n", - "matches" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 838f7e6cdad7b34e872a56afa82f697440bc6c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=C3=A9cio=20Santos?= Date: Tue, 11 Jun 2024 22:49:41 -0400 Subject: [PATCH 09/22] feat(api): add match_values() and preview_value_mappings() --- bdikit/functional_api.py | 319 +- .../value_mapping/algorithms.py | 13 +- .../value_mapping/value_mappers.py | 3 +- examples/getting_started.ipynb | 3111 +++++++++++++++++ tests/test_api.py | 42 +- 5 files changed, 3473 insertions(+), 15 deletions(-) create mode 100644 examples/getting_started.ipynb diff --git a/bdikit/functional_api.py b/bdikit/functional_api.py index c5461ac2..e4f2bbf2 100644 --- a/bdikit/functional_api.py +++ b/bdikit/functional_api.py @@ -1,7 +1,9 @@ from enum import Enum from os.path import join, dirname -from typing import Union, Type, List, Optional +from typing import Union, Type, List, Dict, TypedDict, Set, Optional, Tuple, Callable import pandas as pd +import numpy as np +from bdikit.utils import get_gdc_data from bdikit.download import get_cached_model_or_download from bdikit.mapping_algorithms.column_mapping.algorithms import ( BaseColumnMappingAlgorithm, @@ -18,6 +20,23 @@ from bdikit.mapping_algorithms.scope_reducing._algorithms.contrastive_learning.cl_api import ( ContrastiveLearningAPI, ) +from bdikit.mapping_algorithms.value_mapping.algorithms import ( + ValueMatch, + BaseAlgorithm, + TFIDFAlgorithm, + LLMAlgorithm, + EditAlgorithm, + EmbeddingAlgorithm, + AutoFuzzyJoinAlgorithm, + FastTextAlgorithm, +) +from bdikit.mapping_algorithms.value_mapping.value_mappers import ( + ValueMapper, + FunctionValueMapper, + DictionaryMapper, + IdentityValueMapper, +) + GDC_DATA_PATH = join(dirname(__file__), "./resource/gdc_table.csv") @@ -120,16 +139,52 @@ def top_matches( return pd.concat(dfs, ignore_index=True) +class ValueMatchingMethod(Enum): + TFIDF = ("tfidf", TFIDFAlgorithm) + EDIT = ("edit_distance", EditAlgorithm) + EMBEDDINGS = ("embedding", EmbeddingAlgorithm) + AUTOFJ = ("auto_fuzzy_join", AutoFuzzyJoinAlgorithm) + FASTTEXT = ("fasttext", FastTextAlgorithm) + GPT = ("gpt", LLMAlgorithm) + + def __init__(self, method_name: str, method_class: Type[BaseAlgorithm]): + self.method_name = method_name + self.method_class = method_class + + @staticmethod + def get_instance(method_name: str) -> BaseAlgorithm: + methods = { + method.method_name: method.method_class for method in ValueMatchingMethod + } + try: + return methods[method_name]() + except KeyError: + names = ", ".join(list(methods.keys())) + raise ValueError( + f"The {method_name} algorithm is not supported. " + f"Supported algorithms are: {names}" + ) + + def materialize_mapping( - input_dataframe: pd.DataFrame, target: List[dict] + input_table: pd.DataFrame, mapping_spec: List[dict] ) -> pd.DataFrame: + """ + Takes an input DataFrame and a target mapping specification and returns a + new DataFrame created according to the given target mapping specification. + The mapping specification is a list of dictionaries, where each dictionary + defines one column in the output table and how it is created. It includes + the names of the input (source) and output (target) columns and the value + mapper that is used to transform the values of the input column to the + output column. + """ output_dataframe = pd.DataFrame() - for mapping_spec in target: - from_column_name = mapping_spec["from"] - to_column_name = mapping_spec["to"] - value_mapper = mapping_spec["mapper"] + for column_spec in mapping_spec: + from_column_name = column_spec["source"] + to_column_name = column_spec["target"] + value_mapper = column_spec["mapper"] output_dataframe[to_column_name] = map_column_values( - input_dataframe[from_column_name], to_column_name, value_mapper + input_table[from_column_name], to_column_name, value_mapper ) return output_dataframe @@ -140,3 +195,253 @@ def map_column_values( new_column = value_mapper.map(input_column) new_column.name = target return new_column + + +class ValueMatchingResult(TypedDict): + target: str + matches: List[ValueMatch] + coverage: float + unique_values: Set[str] + unmatch_values: Set[str] + + +def match_values( + source: pd.DataFrame, + target: Union[str, pd.DataFrame], + column_mapping: pd.DataFrame, + method: str = ValueMatchingMethod.EDIT.name, +) -> Dict[str, ValueMatchingResult]: + """ + Maps the values of the dataset columns to the target domain using the given method name. + """ + if isinstance(target, str) and target == "gdc": + column_names = column_mapping["target"].unique().tolist() + target_domain = get_gdc_data(column_names) + elif isinstance(target, pd.DataFrame): + target_domain = { + column_name: target[column_name].unique().tolist() + for column_name in target.columns + } + else: + raise ValueError( + "The target must be a DataFrame or a standard vocabulary name." + ) + + column_mapping_dict = column_mapping.set_index("source")["target"].to_dict() + value_matcher = ValueMatchingMethod.get_instance(method) + matches = _match_values(source, target_domain, column_mapping_dict, value_matcher) + return matches + + +def _match_values( + dataset: pd.DataFrame, + target_domain: Dict[str, Optional[List[str]]], + column_mapping: Dict[str, str], + value_matcher: BaseAlgorithm, +) -> Dict[str, ValueMatchingResult]: + + mapping_results: dict[str, ValueMatchingResult] = {} + + for source_column, target_column in column_mapping.items(): + + # 1. Select candidate columns for value mapping + target_domain_list = target_domain[target_column] + if target_domain_list is None or len(target_domain_list) == 0: + continue + + unique_values = dataset[source_column].unique() + if _skip_values(unique_values): + continue + + # 2. Transform the unique values to lowercase + source_values_dict: Dict[str, str] = { + str(x).strip().lower(): str(x).strip() for x in unique_values + } + target_values_dict: Dict[str, str] = {x.lower(): x for x in target_domain_list} + + # 3. Apply the value matcher to create value mapping dictionaries + matches_lowercase = value_matcher.match( + list(source_values_dict.keys()), list(target_values_dict.keys()) + ) + + # 4. Transform the matches to the original case + matches: List[ValueMatch] = [] + for source_value, target_value, similarity in matches_lowercase: + matches.append( + ValueMatch( + current_value=source_values_dict[source_value], + target_value=target_values_dict[target_value], + similarity=similarity, + ) + ) + + # 5. Calculate the coverage and unmatched values + coverage = len(matches) / len(source_values_dict) + source_values = set(source_values_dict.values()) + match_values = set([x[0] for x in matches]) + + mapping_results[source_column] = ValueMatchingResult( + target=target_column, + matches=matches, + coverage=coverage, + unique_values=source_values, + unmatch_values=source_values - match_values, + ) + + return mapping_results + + +def _skip_values(unique_values: np.ndarray, max_length: int = 50): + if isinstance(unique_values[0], float): + return True + elif len(unique_values) > max_length: + return True + else: + return False + + +def preview_value_mappings( + dataset: pd.DataFrame, + column_mapping: Union[Tuple[str, str], pd.DataFrame], + target: Union[str, pd.DataFrame] = "gdc", + method: str = "edit_distance", +) -> List[Dict]: + """ + Print the value mappings in a human-readable format. + """ + if isinstance(column_mapping, pd.DataFrame): + mapping_df = column_mapping + elif isinstance(column_mapping, tuple): + mapping_df = pd.DataFrame( + [ + { + "source": column_mapping[0], + "target": column_mapping[1], + } + ] + ) + else: + raise ValueError( + "The column_mapping must be a DataFrame or a tuple of two strings." + ) + + value_mappings = match_values( + dataset, target=target, column_mapping=mapping_df, method=method + ) + + result = [] + for source_column, matching_result in value_mappings.items(): + # transform matches and unmatched values into DataFrames + matches_df = pd.DataFrame( + data=matching_result["matches"], + columns=["source", "target", "similarity"], + ) + + unmatched_values = matching_result["unmatch_values"] + unmatched_df = pd.DataFrame( + data=list( + zip( + unmatched_values, + [""] * len(unmatched_values), + [""] * len(unmatched_values), + ) + ), + columns=["source", "target", "similarity"], + ) + + result.append( + { + "source": source_column, + "target": matching_result["target"], + "mapping": pd.concat([matches_df, unmatched_df], ignore_index=True), + } + ) + + if isinstance(column_mapping, tuple): + # If only a single mapping is provided (as a tuple), we return the result + # directly as a DataFrame to make it easier to display it in notebooks. + assert len(result) == 1 + return result[0]["mapping"] + else: + return result + + +def update_mappings(value_mappings: Dict, user_mappings: List) -> List: + user_mappings_dict = { + user_mapping["source"] + "__" + user_mapping["target"]: user_mapping + for user_mapping in user_mappings + } + + final_mappings = [] + for source_column, mapping in value_mappings.items(): + # if the mapping is provided by the user, we ignore it here + # since the user mappings take precedence + key = source_column + "__" + mapping["target"] + if key not in user_mappings_dict: + final_mappings.append( + { + "source": source_column, + "target": mapping["target"], + "mapper": create_mapper(mapping), + } + ) + + # include all user mappings + for user_mapping in user_mappings: + mapper_spec = user_mapping.get("mapper", None) + if not isinstance(mapper_spec, ValueMapper): + user_mapping["mapper"] = create_mapper(mapper_spec) + final_mappings.append(user_mapping) + + return final_mappings + + +def create_mapper( + input: Union[None, pd.DataFrame, Dict, Callable[[pd.Series], pd.Series]] +): + """ + Tries to instantiate an appropriate ValueMapper object for the given input argument. + Depending on the input type, it creates one of the following objects: + - If input is a function (or lambda function), it creates a FunctionValueMapper object. + - If input is a dictionary or Pandas DataFrame, it creates a DictionaryMapper object. + - If input is None, it creates an IdentityValueMapper object. + """ + if input is None: + return IdentityValueMapper() + + if isinstance(input, ValueMapper): + return input + + if callable(input): + return FunctionValueMapper(input) + + if ( + isinstance(input, dict) + and "matches" in input + and isinstance(input["matches"], list) + ): + # This is a dictionary returned by match_values function + matches = input["matches"] + mapping_dict = {} + for match in matches: + if isinstance(match, ValueMatch): + mapping_dict[match.current_value] = match.target_value + elif isinstance(match, tuple) and len(match) >= 2: + if isinstance(match[0], str) and isinstance(match[1], str): + mapping_dict[match[0]] = match[1] + else: + raise ValueError( + "Tuple in matches must contain two strings: (current_value, target_value)" + ) + else: + raise ValueError( + "Matches must be a list of ValueMatch objects or tuples" + ) + return DictionaryMapper(mapping_dict) + + if isinstance(input, pd.DataFrame) and all( + k in input.columns for k in ["current_value", "target_value"] + ): + return DictionaryMapper( + input.set_index("current_value")["target_value"].to_dict() + ) diff --git a/bdikit/mapping_algorithms/value_mapping/algorithms.py b/bdikit/mapping_algorithms/value_mapping/algorithms.py index 0e0f26b1..08e5c438 100644 --- a/bdikit/mapping_algorithms/value_mapping/algorithms.py +++ b/bdikit/mapping_algorithms/value_mapping/algorithms.py @@ -3,7 +3,7 @@ from openai import OpenAI from polyfuzz import PolyFuzz from polyfuzz.models import EditDistance, TFIDF, Embeddings -from flair.embeddings import TransformerWordEmbeddings +from flair.embeddings import TransformerWordEmbeddings, WordEmbeddings from autofj import AutoFJ from Levenshtein import ratio import pandas as pd @@ -91,6 +91,17 @@ def __init__(self, model_path: str = "bert-base-multilingual-cased"): super().__init__(PolyFuzz(method)) +class FastTextAlgorithm(PolyFuzzAlgorithm): + """ + Value matching algorithm based on the cosine similarity of FastText embeddings. + """ + + def __init__(self, model_name: str = "en-crawl"): + embeddings = WordEmbeddings(model_name) + method = Embeddings(embeddings, min_similarity=0) + super().__init__(PolyFuzz(method)) + + class LLMAlgorithm(BaseAlgorithm): def __init__(self): self.client = OpenAI() diff --git a/bdikit/mapping_algorithms/value_mapping/value_mappers.py b/bdikit/mapping_algorithms/value_mapping/value_mappers.py index 4c36f4ad..5b8b635d 100644 --- a/bdikit/mapping_algorithms/value_mapping/value_mappers.py +++ b/bdikit/mapping_algorithms/value_mapping/value_mappers.py @@ -1,4 +1,5 @@ import pandas as pd +from typing import Callable class ValueMapper: @@ -34,7 +35,7 @@ class FunctionValueMapper(ValueMapper): provided custom function. """ - def __init__(self, function): + def __init__(self, function: Callable[[pd.Series], pd.Series]): self.function = function def map(self, input_column: pd.Series) -> pd.Series: diff --git a/examples/getting_started.ipynb b/examples/getting_started.ipynb new file mode 100644 index 00000000..f45b5600 --- /dev/null +++ b/examples/getting_started.ipynb @@ -0,0 +1,3111 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n var force = true;\n var py_version = '3.1.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n var is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = false;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n run_callbacks();\n return null;\n }\n if (!reloading) {\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error() {\n console.error(\"failed to load \" + url);\n }\n\n var skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {'tabulator': 'https://cdn.jsdelivr.net/npm/tabulator-tables@5.5.0/dist/js/tabulator', 'moment': 'https://cdn.jsdelivr.net/npm/luxon/build/global/luxon.min', 'jspanel': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel', 'jspanel-modal': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal', 'jspanel-tooltip': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip', 'jspanel-hint': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint', 'jspanel-layout': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout', 'jspanel-contextmenu': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu', 'jspanel-dock': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock', 'gridstack': 'https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack-all', 'notyf': 'https://cdn.jsdelivr.net/npm/notyf@3/notyf.min'}, 'shim': {'jspanel': {'exports': 'jsPanel'}, 'gridstack': {'exports': 'GridStack'}}});\n require([\"tabulator\"], function(Tabulator) {\n\twindow.Tabulator = Tabulator\n\ton_load()\n })\n require([\"moment\"], function(moment) {\n\twindow.moment = moment\n\ton_load()\n })\n require([\"jspanel\"], function(jsPanel) {\n\twindow.jsPanel = jsPanel\n\ton_load()\n })\n require([\"jspanel-modal\"], function() {\n\ton_load()\n })\n require([\"jspanel-tooltip\"], function() {\n\ton_load()\n })\n require([\"jspanel-hint\"], function() {\n\ton_load()\n })\n require([\"jspanel-layout\"], function() {\n\ton_load()\n })\n require([\"jspanel-contextmenu\"], function() {\n\ton_load()\n })\n require([\"jspanel-dock\"], function() {\n\ton_load()\n })\n require([\"gridstack\"], function(GridStack) {\n\twindow.GridStack = GridStack\n\ton_load()\n })\n require([\"notyf\"], function() {\n\ton_load()\n })\n root._bokeh_is_loading = css_urls.length + 11;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n var existing_stylesheets = []\n var links = document.getElementsByTagName('link')\n for (var i = 0; i < links.length; i++) {\n var link = links[i]\n if (link.href != null) {\n\texisting_stylesheets.push(link.href)\n }\n }\n for (var i = 0; i < css_urls.length; i++) {\n var url = css_urls[i];\n if (existing_stylesheets.indexOf(url) !== -1) {\n\ton_load()\n\tcontinue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } if (((window['Tabulator'] !== undefined) && (!(window['Tabulator'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['moment'] !== undefined) && (!(window['moment'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/datatabulator/luxon/build/global/luxon.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['jsPanel'] !== undefined) && (!(window['jsPanel'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['GridStack'] !== undefined) && (!(window['GridStack'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/gridstack/gridstack@7.2.3/dist/gridstack-all.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['Notyf'] !== undefined) && (!(window['Notyf'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/notificationarea/notyf@3/notyf.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } var existing_scripts = []\n var scripts = document.getElementsByTagName('script')\n for (var i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n\texisting_scripts.push(script.src)\n }\n }\n for (var i = 0; i < js_urls.length; i++) {\n var url = js_urls[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (var i = 0; i < js_modules.length; i++) {\n var url = js_modules[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n var url = js_exports[name];\n if (skip.indexOf(url) >= 0 || root[name] != null) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n var js_urls = [\"https://cdn.holoviz.org/panel/1.2.3/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.js\", \"https://cdn.holoviz.org/panel/1.2.3/dist/bundled/datatabulator/luxon/build/global/luxon.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.1.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.1.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.1.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.1.1.min.js\", \"https://cdn.holoviz.org/panel/1.2.3/dist/panel.min.js\"];\n var js_modules = [];\n var js_exports = {};\n var css_urls = [\"https://cdn.holoviz.org/panel/1.2.3/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_simple.min.css\"];\n var inline_js = [ function(Bokeh) {\n inject_raw_css(\".tabulator{position:relative;border:1px solid #999;font-size:14px;text-align:left;overflow:hidden;-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}.tabulator[tabulator-layout=fitDataFill] .tabulator-tableholder .tabulator-table{min-width:100%}.tabulator[tabulator-layout=fitDataTable]{display:inline-block}.tabulator.tabulator-block-select{user-select:none}.tabulator .tabulator-header{position:relative;box-sizing:border-box;width:100%;border-bottom:1px solid #999;background-color:#fff;color:#555;font-weight:700;white-space:nowrap;overflow:hidden;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none}.tabulator .tabulator-header.tabulator-header-hidden{display:none}.tabulator .tabulator-header .tabulator-header-contents{position:relative;overflow:hidden}.tabulator .tabulator-header .tabulator-header-contents .tabulator-headers{display:inline-block}.tabulator .tabulator-header .tabulator-col{display:inline-flex;position:relative;box-sizing:border-box;flex-direction:column;justify-content:flex-start;border-right:1px solid #ddd;background:#fff;text-align:left;vertical-align:bottom;overflow:hidden}.tabulator .tabulator-header .tabulator-col.tabulator-moving{position:absolute;border:1px solid #999;background:#e6e6e6;pointer-events:none}.tabulator .tabulator-header .tabulator-col .tabulator-col-content{box-sizing:border-box;position:relative;padding:4px}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-header-popup-button{padding:0 8px}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-header-popup-button:hover{cursor:pointer;opacity:.6}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title-holder{position:relative}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title{box-sizing:border-box;width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title.tabulator-col-title-wrap{white-space:normal;text-overflow:clip}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title .tabulator-title-editor{box-sizing:border-box;width:100%;border:1px solid #999;padding:1px;background:#fff}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title .tabulator-header-popup-button+.tabulator-title-editor{width:calc(100% - 22px)}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter{display:flex;align-items:center;position:absolute;top:0;bottom:0;right:4px}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #bbb}.tabulator .tabulator-header .tabulator-col.tabulator-col-group .tabulator-col-group-cols{position:relative;display:flex;border-top:1px solid #ddd;overflow:hidden;margin-right:-1px}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter{position:relative;box-sizing:border-box;margin-top:2px;width:100%;text-align:center}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter textarea{height:auto!important}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter svg{margin-top:3px}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter input::-ms-clear{width:0;height:0}.tabulator .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title{padding-right:25px}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover{cursor:pointer;background-color:#e6e6e6}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter{color:#bbb}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{cursor:pointer;border-bottom:6px solid #555}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-top:none;border-bottom:6px solid #bbb}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter{color:#666}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{cursor:pointer;border-bottom:6px solid #555}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-top:none;border-bottom:6px solid #666}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter{color:#666}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{cursor:pointer;border-top:6px solid #555}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-bottom:none;border-top:6px solid #666;color:#666}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical .tabulator-col-content .tabulator-col-title{writing-mode:vertical-rl;text-orientation:mixed;display:flex;align-items:center;justify-content:center}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-col-vertical-flip .tabulator-col-title{transform:rotate(180deg)}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable .tabulator-col-title{padding-right:0;padding-top:20px}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable.tabulator-col-vertical-flip .tabulator-col-title{padding-right:0;padding-bottom:20px}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable .tabulator-col-sorter{justify-content:center;left:0;right:0;top:4px;bottom:auto}.tabulator .tabulator-header .tabulator-frozen{position:sticky;left:0;z-index:10}.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left{border-right:2px solid #ddd}.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-right{border-left:2px solid #ddd}.tabulator .tabulator-header .tabulator-calcs-holder{box-sizing:border-box;background:#fff!important;border-top:1px solid #ddd;border-bottom:1px solid #ddd}.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row{background:#fff!important}.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row .tabulator-col-resize-handle,.tabulator .tabulator-header .tabulator-frozen-rows-holder:empty{display:none}.tabulator .tabulator-tableholder{position:relative;width:100%;white-space:nowrap;overflow:auto;-webkit-overflow-scrolling:touch}.tabulator .tabulator-tableholder:focus{outline:none}.tabulator .tabulator-tableholder .tabulator-placeholder{box-sizing:border-box;display:flex;align-items:center;justify-content:center;width:100%}.tabulator .tabulator-tableholder .tabulator-placeholder[tabulator-render-mode=virtual]{min-height:100%;min-width:100%}.tabulator .tabulator-tableholder .tabulator-placeholder .tabulator-placeholder-contents{display:inline-block;text-align:center;padding:10px;color:#ccc;font-weight:700;font-size:20px;white-space:normal}.tabulator .tabulator-tableholder .tabulator-table{position:relative;display:inline-block;background-color:#fff;white-space:nowrap;overflow:visible;color:#333}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs{font-weight:700;background:#f2f2f2!important}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs.tabulator-calcs-top{border-bottom:2px solid #ddd}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs.tabulator-calcs-bottom{border-top:2px solid #ddd}.tabulator .tabulator-footer{border-top:1px solid #999;background-color:#fff;color:#555;font-weight:700;white-space:nowrap;user-select:none;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none}.tabulator .tabulator-footer .tabulator-footer-contents{display:flex;flex-direction:row;align-items:center;justify-content:space-between;padding:5px 10px}.tabulator .tabulator-footer .tabulator-footer-contents:empty{display:none}.tabulator .tabulator-footer .tabulator-calcs-holder{box-sizing:border-box;width:100%;text-align:left;background:#fff!important;border-bottom:1px solid #ddd;border-top:1px solid #ddd;overflow:hidden}.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row{display:inline-block;background:#fff!important}.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row .tabulator-col-resize-handle{display:none}.tabulator .tabulator-footer .tabulator-calcs-holder:only-child{margin-bottom:-5px;border-bottom:none}.tabulator .tabulator-footer>*+.tabulator-page-counter{margin-left:10px}.tabulator .tabulator-footer .tabulator-page-counter{font-weight:400}.tabulator .tabulator-footer .tabulator-paginator{flex:1;text-align:right;color:#555;font-family:inherit;font-weight:inherit;font-size:inherit}.tabulator .tabulator-footer .tabulator-page-size{display:inline-block;margin:0 5px;padding:2px 5px;border:1px solid #aaa;border-radius:3px}.tabulator .tabulator-footer .tabulator-pages{margin:0 7px}.tabulator .tabulator-footer .tabulator-page{display:inline-block;margin:0 2px;padding:2px 5px;border:1px solid #aaa;border-radius:3px;background:hsla(0,0%,100%,.2)}.tabulator .tabulator-footer .tabulator-page.active{color:#d00}.tabulator .tabulator-footer .tabulator-page:disabled{opacity:.5}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-footer .tabulator-page:not(.disabled):hover{cursor:pointer;background:rgba(0,0,0,.2);color:#fff}}.tabulator .tabulator-col-resize-handle{position:relative;display:inline-block;width:6px;margin-left:-3px;margin-right:-3px;z-index:10;vertical-align:middle}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-col-resize-handle:hover{cursor:ew-resize}}.tabulator .tabulator-col-resize-handle:last-of-type{width:3px;margin-right:0}.tabulator .tabulator-alert{position:absolute;display:flex;align-items:center;top:0;left:0;z-index:100;height:100%;width:100%;background:rgba(0,0,0,.4);text-align:center}.tabulator .tabulator-alert .tabulator-alert-msg{display:inline-block;margin:0 auto;padding:10px 20px;border-radius:10px;background:#fff;font-weight:700;font-size:16px}.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-msg{border:4px solid #333;color:#000}.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-error{border:4px solid #d00;color:#590000}.tabulator-row{position:relative;box-sizing:border-box;min-height:22px}.tabulator-row,.tabulator-row.tabulator-row-even{background-color:#fff}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-selectable:hover{background-color:#bbb;cursor:pointer}}.tabulator-row.tabulator-selected{background-color:#9abcea}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-selected:hover{background-color:#769bcc;cursor:pointer}}.tabulator-row.tabulator-row-moving{border:1px solid #000;background:#fff}.tabulator-row.tabulator-moving{position:absolute;border-top:1px solid #ddd;border-bottom:1px solid #ddd;pointer-events:none;z-index:15}.tabulator-row .tabulator-row-resize-handle{position:absolute;right:0;bottom:0;left:0;height:5px}.tabulator-row .tabulator-row-resize-handle.prev{top:0;bottom:auto}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-row-resize-handle:hover{cursor:ns-resize}}.tabulator-row .tabulator-responsive-collapse{box-sizing:border-box;padding:5px;border-top:1px solid #ddd;border-bottom:1px solid #ddd}.tabulator-row .tabulator-responsive-collapse:empty{display:none}.tabulator-row .tabulator-responsive-collapse table{font-size:14px}.tabulator-row .tabulator-responsive-collapse table tr td{position:relative}.tabulator-row .tabulator-responsive-collapse table tr td:first-of-type{padding-right:10px}.tabulator-row .tabulator-cell{display:inline-block;position:relative;box-sizing:border-box;padding:4px;border-right:1px solid #ddd;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tabulator-row .tabulator-cell.tabulator-frozen{display:inline-block;position:sticky;left:0;background-color:inherit;z-index:10}.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left{border-right:2px solid #ddd}.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right{border-left:2px solid #ddd}.tabulator-row .tabulator-cell.tabulator-editing{border:1px solid #1d68cd;outline:none;padding:0}.tabulator-row .tabulator-cell.tabulator-editing input,.tabulator-row .tabulator-cell.tabulator-editing select{border:1px;background:transparent;outline:none}.tabulator-row .tabulator-cell.tabulator-validation-fail{border:1px solid #d00}.tabulator-row .tabulator-cell.tabulator-validation-fail input,.tabulator-row .tabulator-cell.tabulator-validation-fail select{border:1px;background:transparent;color:#d00}.tabulator-row .tabulator-cell.tabulator-row-handle{display:inline-flex;align-items:center;justify-content:center;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none}.tabulator-row .tabulator-cell.tabulator-row-handle .tabulator-row-handle-box{width:80%}.tabulator-row .tabulator-cell.tabulator-row-handle .tabulator-row-handle-box .tabulator-row-handle-bar{width:100%;height:3px;margin-top:2px;background:#666}.tabulator-row .tabulator-cell .tabulator-data-tree-branch{display:inline-block;vertical-align:middle;height:9px;width:7px;margin-top:-9px;margin-right:5px;border-bottom-left-radius:1px;border-left:2px solid #ddd;border-bottom:2px solid #ddd}.tabulator-row .tabulator-cell .tabulator-data-tree-control{display:inline-flex;justify-content:center;align-items:center;vertical-align:middle;height:11px;width:11px;margin-right:5px;border:1px solid #333;border-radius:2px;background:rgba(0,0,0,.1);overflow:hidden}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-cell .tabulator-data-tree-control:hover{cursor:pointer;background:rgba(0,0,0,.2)}}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-collapse{display:inline-block;position:relative;height:7px;width:1px;background:transparent}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-collapse:after{position:absolute;content:\\\"\\\";left:-3px;top:3px;height:1px;width:7px;background:#333}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-expand{display:inline-block;position:relative;height:7px;width:1px;background:#333}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-expand:after{position:absolute;content:\\\"\\\";left:-3px;top:3px;height:1px;width:7px;background:#333}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle{display:inline-flex;align-items:center;justify-content:center;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none;height:15px;width:15px;border-radius:20px;background:#666;color:#fff;font-weight:700;font-size:1.1em}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle:hover{opacity:.7;cursor:pointer}}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle.open .tabulator-responsive-collapse-toggle-close{display:initial}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle.open .tabulator-responsive-collapse-toggle-open{display:none}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle svg{stroke:#fff}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle .tabulator-responsive-collapse-toggle-close{display:none}.tabulator-row .tabulator-cell .tabulator-traffic-light{display:inline-block;height:14px;width:14px;border-radius:14px}.tabulator-row.tabulator-group{box-sizing:border-box;border-bottom:1px solid #999;border-right:1px solid #ddd;border-top:1px solid #999;padding:5px 5px 5px 10px;background:#ccc;font-weight:700;min-width:100%}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-group:hover{cursor:pointer;background-color:rgba(0,0,0,.1)}}.tabulator-row.tabulator-group.tabulator-group-visible .tabulator-arrow{margin-right:10px;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid #666;border-bottom:0}.tabulator-row.tabulator-group.tabulator-group-level-1{padding-left:30px}.tabulator-row.tabulator-group.tabulator-group-level-2{padding-left:50px}.tabulator-row.tabulator-group.tabulator-group-level-3{padding-left:70px}.tabulator-row.tabulator-group.tabulator-group-level-4{padding-left:90px}.tabulator-row.tabulator-group.tabulator-group-level-5{padding-left:110px}.tabulator-row.tabulator-group .tabulator-group-toggle{display:inline-block}.tabulator-row.tabulator-group .tabulator-arrow{display:inline-block;width:0;height:0;margin-right:16px;border-top:6px solid transparent;border-bottom:6px solid transparent;border-right:0;border-left:6px solid #666;vertical-align:middle}.tabulator-row.tabulator-group span{margin-left:10px;color:#d00}.tabulator-popup-container{position:absolute;display:inline-block;box-sizing:border-box;background:#fff;border:1px solid #ddd;box-shadow:0 0 5px 0 rgba(0,0,0,.2);font-size:14px;overflow-y:auto;-webkit-overflow-scrolling:touch;z-index:10000}.tabulator-popup{padding:5px;border-radius:3px}.tabulator-tooltip{max-width:Min(500px,100%);padding:3px 5px;border-radius:2px;box-shadow:none;font-size:12px;pointer-events:none}.tabulator-menu .tabulator-menu-item{position:relative;box-sizing:border-box;padding:5px 10px;user-select:none}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-disabled{opacity:.5}@media (hover:hover) and (pointer:fine){.tabulator-menu .tabulator-menu-item:not(.tabulator-menu-item-disabled):hover{cursor:pointer;background:#fff}}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-submenu{padding-right:25px}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-submenu:after{display:inline-block;position:absolute;top:calc(5px + .4em);right:10px;height:7px;width:7px;content:\\\"\\\";border-color:#ddd;border-style:solid;border-width:1px 1px 0 0;vertical-align:top;transform:rotate(45deg)}.tabulator-menu .tabulator-menu-separator{border-top:1px solid #ddd}.tabulator-edit-list{max-height:200px;font-size:14px;overflow-y:auto;-webkit-overflow-scrolling:touch}.tabulator-edit-list .tabulator-edit-list-item{padding:4px;color:#333;outline:none}.tabulator-edit-list .tabulator-edit-list-item.active{color:#fff;background:#1d68cd}.tabulator-edit-list .tabulator-edit-list-item.active.focused{outline:1px solid hsla(0,0%,100%,.5)}.tabulator-edit-list .tabulator-edit-list-item.focused{outline:1px solid #1d68cd}@media (hover:hover) and (pointer:fine){.tabulator-edit-list .tabulator-edit-list-item:hover{cursor:pointer;color:#fff;background:#1d68cd}}.tabulator-edit-list .tabulator-edit-list-placeholder{padding:4px;color:#333;text-align:center}.tabulator-edit-list .tabulator-edit-list-group{border-bottom:1px solid #ddd;padding:6px 4px 4px;color:#333;font-weight:700}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-2,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-2{padding-left:12px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-3,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-3{padding-left:20px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-4,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-4{padding-left:28px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-5,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-5{padding-left:36px}.tabulator.tabulator-ltr{direction:ltr}.tabulator.tabulator-rtl{text-align:initial;direction:rtl}.tabulator.tabulator-rtl .tabulator-header .tabulator-col{text-align:initial;border-left:1px solid #ddd;border-right:initial}.tabulator.tabulator-rtl .tabulator-header .tabulator-col.tabulator-col-group .tabulator-col-group-cols{margin-right:0;margin-left:-1px}.tabulator.tabulator-rtl .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title{padding-right:0;padding-left:25px}.tabulator.tabulator-rtl .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter{left:8px;right:auto}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell{border-right:initial;border-left:1px solid #ddd}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell .tabulator-data-tree-branch{margin-right:0;margin-left:5px;border-bottom-left-radius:0;border-bottom-right-radius:1px;border-left:initial;border-right:2px solid #ddd}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell .tabulator-data-tree-control{margin-right:0;margin-left:5px}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left{border-left:2px solid #ddd}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right{border-right:2px solid #ddd}.tabulator.tabulator-rtl .tabulator-row .tabulator-col-resize-handle:last-of-type{width:3px;margin-left:0;margin-right:-3px}.tabulator.tabulator-rtl .tabulator-footer .tabulator-calcs-holder{text-align:initial}.tabulator-print-fullscreen{position:absolute;top:0;bottom:0;left:0;right:0;z-index:10000}body.tabulator-print-fullscreen-hide>:not(.tabulator-print-fullscreen){display:none!important}.tabulator-print-table{border-collapse:collapse}.tabulator-print-table .tabulator-data-tree-branch{display:inline-block;vertical-align:middle;height:9px;width:7px;margin-top:-9px;margin-right:5px;border-bottom-left-radius:1px;border-left:2px solid #ddd;border-bottom:2px solid #ddd}.tabulator-print-table .tabulator-print-table-group{box-sizing:border-box;border-bottom:1px solid #999;border-right:1px solid #ddd;border-top:1px solid #999;padding:5px 5px 5px 10px;background:#ccc;font-weight:700;min-width:100%}@media (hover:hover) and (pointer:fine){.tabulator-print-table .tabulator-print-table-group:hover{cursor:pointer;background-color:rgba(0,0,0,.1)}}.tabulator-print-table .tabulator-print-table-group.tabulator-group-visible .tabulator-arrow{margin-right:10px;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid #666;border-bottom:0}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-1 td{padding-left:30px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-2 td{padding-left:50px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-3 td{padding-left:70px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-4 td{padding-left:90px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-5 td{padding-left:110px!important}.tabulator-print-table .tabulator-print-table-group .tabulator-group-toggle{display:inline-block}.tabulator-print-table .tabulator-print-table-group .tabulator-arrow{display:inline-block;width:0;height:0;margin-right:16px;border-top:6px solid transparent;border-bottom:6px solid transparent;border-right:0;border-left:6px solid #666;vertical-align:middle}.tabulator-print-table .tabulator-print-table-group span{color:#d00}.tabulator-print-table .tabulator-data-tree-control{display:inline-flex;justify-content:center;align-items:center;vertical-align:middle;height:11px;width:11px;margin-right:5px;border:1px solid #333;border-radius:2px;background:rgba(0,0,0,.1);overflow:hidden}@media (hover:hover) and (pointer:fine){.tabulator-print-table .tabulator-data-tree-control:hover{cursor:pointer;background:rgba(0,0,0,.2)}}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-collapse{display:inline-block;position:relative;height:7px;width:1px;background:transparent}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-collapse:after{position:absolute;content:\\\"\\\";left:-3px;top:3px;height:1px;width:7px;background:#333}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-expand{display:inline-block;position:relative;height:7px;width:1px;background:#333}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-expand:after{position:absolute;content:\\\"\\\";left:-3px;top:3px;height:1px;width:7px;background:#333}.tabulator{border:none;background-color:#fff}.tabulator .tabulator-header .tabulator-calcs-holder{background:#f2f2f2!important;border-bottom:1px solid #999}.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row{background:#f2f2f2!important}.tabulator .tabulator-tableholder .tabulator-placeholder span{color:#000}.tabulator .tabulator-footer .tabulator-calcs-holder{background:#f2f2f2!important;border-bottom:1px solid #fff}.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row{background:#f2f2f2!important}.tabulator-row{border-bottom:1px solid #ddd}.tabulator-row .tabulator-cell:last-of-type{border-right:none}.tabulator-row.tabulator-group span{color:#666}.tabulator-print-table .tabulator-print-table-group span{margin-left:10px;color:#666}\\n/*# sourceMappingURL=tabulator_simple.min.css.map */\");\n }, function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (var i = 0; i < inline_js.length; i++) {\n inline_js[i].call(root, root.Bokeh);\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n\tvar NewBokeh = root.Bokeh;\n\tif (Bokeh.versions === undefined) {\n\t Bokeh.versions = new Map();\n\t}\n\tif (NewBokeh.version !== Bokeh.version) {\n\t Bokeh.versions.set(NewBokeh.version, NewBokeh)\n\t}\n\troot.Bokeh = Bokeh;\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\n\troot.Bokeh = undefined;\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n\tconsole.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n\trun_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n comm.open();\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n }) \n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ] + }, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "ef90f212-6a52-4dc3-8d87-f3b72292670f" + } + }, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n var force = true;\n var py_version = '3.1.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n var is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n run_callbacks();\n return null;\n }\n if (!reloading) {\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error() {\n console.error(\"failed to load \" + url);\n }\n\n var skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {'tabulator': 'https://cdn.jsdelivr.net/npm/tabulator-tables@5.5.0/dist/js/tabulator', 'moment': 'https://cdn.jsdelivr.net/npm/luxon/build/global/luxon.min', 'mathjax': '//cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS_HTML', 'jspanel': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel', 'jspanel-modal': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal', 'jspanel-tooltip': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip', 'jspanel-hint': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint', 'jspanel-layout': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout', 'jspanel-contextmenu': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu', 'jspanel-dock': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock', 'gridstack': 'https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack-all', 'notyf': 'https://cdn.jsdelivr.net/npm/notyf@3/notyf.min'}, 'shim': {'mathjax': {'exports': 'MathJax'}, 'jspanel': {'exports': 'jsPanel'}, 'gridstack': {'exports': 'GridStack'}}});\n require([\"tabulator\"], function(Tabulator) {\n\twindow.Tabulator = Tabulator\n\ton_load()\n })\n require([\"moment\"], function(moment) {\n\twindow.moment = moment\n\ton_load()\n })\n require([\"mathjax\"], function() {\n\ton_load()\n })\n require([\"jspanel\"], function(jsPanel) {\n\twindow.jsPanel = jsPanel\n\ton_load()\n })\n require([\"jspanel-modal\"], function() {\n\ton_load()\n })\n require([\"jspanel-tooltip\"], function() {\n\ton_load()\n })\n require([\"jspanel-hint\"], function() {\n\ton_load()\n })\n require([\"jspanel-layout\"], function() {\n\ton_load()\n })\n require([\"jspanel-contextmenu\"], function() {\n\ton_load()\n })\n require([\"jspanel-dock\"], function() {\n\ton_load()\n })\n require([\"gridstack\"], function(GridStack) {\n\twindow.GridStack = GridStack\n\ton_load()\n })\n require([\"notyf\"], function() {\n\ton_load()\n })\n root._bokeh_is_loading = css_urls.length + 12;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n var existing_stylesheets = []\n var links = document.getElementsByTagName('link')\n for (var i = 0; i < links.length; i++) {\n var link = links[i]\n if (link.href != null) {\n\texisting_stylesheets.push(link.href)\n }\n }\n for (var i = 0; i < css_urls.length; i++) {\n var url = css_urls[i];\n if (existing_stylesheets.indexOf(url) !== -1) {\n\ton_load()\n\tcontinue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } if (((window['Tabulator'] !== undefined) && (!(window['Tabulator'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['moment'] !== undefined) && (!(window['moment'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/datatabulator/luxon/build/global/luxon.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['MathJax'] !== undefined) && (!(window['MathJax'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['jsPanel'] !== undefined) && (!(window['jsPanel'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['GridStack'] !== undefined) && (!(window['GridStack'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/gridstack/gridstack@7.2.3/dist/gridstack-all.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['Notyf'] !== undefined) && (!(window['Notyf'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/notificationarea/notyf@3/notyf.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } var existing_scripts = []\n var scripts = document.getElementsByTagName('script')\n for (var i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n\texisting_scripts.push(script.src)\n }\n }\n for (var i = 0; i < js_urls.length; i++) {\n var url = js_urls[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (var i = 0; i < js_modules.length; i++) {\n var url = js_modules[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n var url = js_exports[name];\n if (skip.indexOf(url) >= 0 || root[name] != null) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n var js_urls = [\"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML\", \"https://cdn.holoviz.org/panel/1.2.3/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.js\", \"https://cdn.holoviz.org/panel/1.2.3/dist/bundled/datatabulator/luxon/build/global/luxon.min.js\"];\n var js_modules = [];\n var js_exports = {};\n var css_urls = [\"https://cdn.holoviz.org/panel/1.2.3/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_simple.min.css\"];\n var inline_js = [ function(Bokeh) {\n inject_raw_css(\".tabulator{position:relative;border:1px solid #999;font-size:14px;text-align:left;overflow:hidden;-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}.tabulator[tabulator-layout=fitDataFill] .tabulator-tableholder .tabulator-table{min-width:100%}.tabulator[tabulator-layout=fitDataTable]{display:inline-block}.tabulator.tabulator-block-select{user-select:none}.tabulator .tabulator-header{position:relative;box-sizing:border-box;width:100%;border-bottom:1px solid #999;background-color:#fff;color:#555;font-weight:700;white-space:nowrap;overflow:hidden;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none}.tabulator .tabulator-header.tabulator-header-hidden{display:none}.tabulator .tabulator-header .tabulator-header-contents{position:relative;overflow:hidden}.tabulator .tabulator-header .tabulator-header-contents .tabulator-headers{display:inline-block}.tabulator .tabulator-header .tabulator-col{display:inline-flex;position:relative;box-sizing:border-box;flex-direction:column;justify-content:flex-start;border-right:1px solid #ddd;background:#fff;text-align:left;vertical-align:bottom;overflow:hidden}.tabulator .tabulator-header .tabulator-col.tabulator-moving{position:absolute;border:1px solid #999;background:#e6e6e6;pointer-events:none}.tabulator .tabulator-header .tabulator-col .tabulator-col-content{box-sizing:border-box;position:relative;padding:4px}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-header-popup-button{padding:0 8px}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-header-popup-button:hover{cursor:pointer;opacity:.6}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title-holder{position:relative}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title{box-sizing:border-box;width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title.tabulator-col-title-wrap{white-space:normal;text-overflow:clip}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title .tabulator-title-editor{box-sizing:border-box;width:100%;border:1px solid #999;padding:1px;background:#fff}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title .tabulator-header-popup-button+.tabulator-title-editor{width:calc(100% - 22px)}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter{display:flex;align-items:center;position:absolute;top:0;bottom:0;right:4px}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #bbb}.tabulator .tabulator-header .tabulator-col.tabulator-col-group .tabulator-col-group-cols{position:relative;display:flex;border-top:1px solid #ddd;overflow:hidden;margin-right:-1px}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter{position:relative;box-sizing:border-box;margin-top:2px;width:100%;text-align:center}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter textarea{height:auto!important}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter svg{margin-top:3px}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter input::-ms-clear{width:0;height:0}.tabulator .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title{padding-right:25px}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover{cursor:pointer;background-color:#e6e6e6}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter{color:#bbb}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{cursor:pointer;border-bottom:6px solid #555}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-top:none;border-bottom:6px solid #bbb}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter{color:#666}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{cursor:pointer;border-bottom:6px solid #555}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-top:none;border-bottom:6px solid #666}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter{color:#666}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{cursor:pointer;border-top:6px solid #555}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-bottom:none;border-top:6px solid #666;color:#666}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical .tabulator-col-content .tabulator-col-title{writing-mode:vertical-rl;text-orientation:mixed;display:flex;align-items:center;justify-content:center}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-col-vertical-flip .tabulator-col-title{transform:rotate(180deg)}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable .tabulator-col-title{padding-right:0;padding-top:20px}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable.tabulator-col-vertical-flip .tabulator-col-title{padding-right:0;padding-bottom:20px}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable .tabulator-col-sorter{justify-content:center;left:0;right:0;top:4px;bottom:auto}.tabulator .tabulator-header .tabulator-frozen{position:sticky;left:0;z-index:10}.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left{border-right:2px solid #ddd}.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-right{border-left:2px solid #ddd}.tabulator .tabulator-header .tabulator-calcs-holder{box-sizing:border-box;background:#fff!important;border-top:1px solid #ddd;border-bottom:1px solid #ddd}.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row{background:#fff!important}.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row .tabulator-col-resize-handle,.tabulator .tabulator-header .tabulator-frozen-rows-holder:empty{display:none}.tabulator .tabulator-tableholder{position:relative;width:100%;white-space:nowrap;overflow:auto;-webkit-overflow-scrolling:touch}.tabulator .tabulator-tableholder:focus{outline:none}.tabulator .tabulator-tableholder .tabulator-placeholder{box-sizing:border-box;display:flex;align-items:center;justify-content:center;width:100%}.tabulator .tabulator-tableholder .tabulator-placeholder[tabulator-render-mode=virtual]{min-height:100%;min-width:100%}.tabulator .tabulator-tableholder .tabulator-placeholder .tabulator-placeholder-contents{display:inline-block;text-align:center;padding:10px;color:#ccc;font-weight:700;font-size:20px;white-space:normal}.tabulator .tabulator-tableholder .tabulator-table{position:relative;display:inline-block;background-color:#fff;white-space:nowrap;overflow:visible;color:#333}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs{font-weight:700;background:#f2f2f2!important}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs.tabulator-calcs-top{border-bottom:2px solid #ddd}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs.tabulator-calcs-bottom{border-top:2px solid #ddd}.tabulator .tabulator-footer{border-top:1px solid #999;background-color:#fff;color:#555;font-weight:700;white-space:nowrap;user-select:none;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none}.tabulator .tabulator-footer .tabulator-footer-contents{display:flex;flex-direction:row;align-items:center;justify-content:space-between;padding:5px 10px}.tabulator .tabulator-footer .tabulator-footer-contents:empty{display:none}.tabulator .tabulator-footer .tabulator-calcs-holder{box-sizing:border-box;width:100%;text-align:left;background:#fff!important;border-bottom:1px solid #ddd;border-top:1px solid #ddd;overflow:hidden}.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row{display:inline-block;background:#fff!important}.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row .tabulator-col-resize-handle{display:none}.tabulator .tabulator-footer .tabulator-calcs-holder:only-child{margin-bottom:-5px;border-bottom:none}.tabulator .tabulator-footer>*+.tabulator-page-counter{margin-left:10px}.tabulator .tabulator-footer .tabulator-page-counter{font-weight:400}.tabulator .tabulator-footer .tabulator-paginator{flex:1;text-align:right;color:#555;font-family:inherit;font-weight:inherit;font-size:inherit}.tabulator .tabulator-footer .tabulator-page-size{display:inline-block;margin:0 5px;padding:2px 5px;border:1px solid #aaa;border-radius:3px}.tabulator .tabulator-footer .tabulator-pages{margin:0 7px}.tabulator .tabulator-footer .tabulator-page{display:inline-block;margin:0 2px;padding:2px 5px;border:1px solid #aaa;border-radius:3px;background:hsla(0,0%,100%,.2)}.tabulator .tabulator-footer .tabulator-page.active{color:#d00}.tabulator .tabulator-footer .tabulator-page:disabled{opacity:.5}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-footer .tabulator-page:not(.disabled):hover{cursor:pointer;background:rgba(0,0,0,.2);color:#fff}}.tabulator .tabulator-col-resize-handle{position:relative;display:inline-block;width:6px;margin-left:-3px;margin-right:-3px;z-index:10;vertical-align:middle}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-col-resize-handle:hover{cursor:ew-resize}}.tabulator .tabulator-col-resize-handle:last-of-type{width:3px;margin-right:0}.tabulator .tabulator-alert{position:absolute;display:flex;align-items:center;top:0;left:0;z-index:100;height:100%;width:100%;background:rgba(0,0,0,.4);text-align:center}.tabulator .tabulator-alert .tabulator-alert-msg{display:inline-block;margin:0 auto;padding:10px 20px;border-radius:10px;background:#fff;font-weight:700;font-size:16px}.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-msg{border:4px solid #333;color:#000}.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-error{border:4px solid #d00;color:#590000}.tabulator-row{position:relative;box-sizing:border-box;min-height:22px}.tabulator-row,.tabulator-row.tabulator-row-even{background-color:#fff}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-selectable:hover{background-color:#bbb;cursor:pointer}}.tabulator-row.tabulator-selected{background-color:#9abcea}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-selected:hover{background-color:#769bcc;cursor:pointer}}.tabulator-row.tabulator-row-moving{border:1px solid #000;background:#fff}.tabulator-row.tabulator-moving{position:absolute;border-top:1px solid #ddd;border-bottom:1px solid #ddd;pointer-events:none;z-index:15}.tabulator-row .tabulator-row-resize-handle{position:absolute;right:0;bottom:0;left:0;height:5px}.tabulator-row .tabulator-row-resize-handle.prev{top:0;bottom:auto}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-row-resize-handle:hover{cursor:ns-resize}}.tabulator-row .tabulator-responsive-collapse{box-sizing:border-box;padding:5px;border-top:1px solid #ddd;border-bottom:1px solid #ddd}.tabulator-row .tabulator-responsive-collapse:empty{display:none}.tabulator-row .tabulator-responsive-collapse table{font-size:14px}.tabulator-row .tabulator-responsive-collapse table tr td{position:relative}.tabulator-row .tabulator-responsive-collapse table tr td:first-of-type{padding-right:10px}.tabulator-row .tabulator-cell{display:inline-block;position:relative;box-sizing:border-box;padding:4px;border-right:1px solid #ddd;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tabulator-row .tabulator-cell.tabulator-frozen{display:inline-block;position:sticky;left:0;background-color:inherit;z-index:10}.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left{border-right:2px solid #ddd}.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right{border-left:2px solid #ddd}.tabulator-row .tabulator-cell.tabulator-editing{border:1px solid #1d68cd;outline:none;padding:0}.tabulator-row .tabulator-cell.tabulator-editing input,.tabulator-row .tabulator-cell.tabulator-editing select{border:1px;background:transparent;outline:none}.tabulator-row .tabulator-cell.tabulator-validation-fail{border:1px solid #d00}.tabulator-row .tabulator-cell.tabulator-validation-fail input,.tabulator-row .tabulator-cell.tabulator-validation-fail select{border:1px;background:transparent;color:#d00}.tabulator-row .tabulator-cell.tabulator-row-handle{display:inline-flex;align-items:center;justify-content:center;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none}.tabulator-row .tabulator-cell.tabulator-row-handle .tabulator-row-handle-box{width:80%}.tabulator-row .tabulator-cell.tabulator-row-handle .tabulator-row-handle-box .tabulator-row-handle-bar{width:100%;height:3px;margin-top:2px;background:#666}.tabulator-row .tabulator-cell .tabulator-data-tree-branch{display:inline-block;vertical-align:middle;height:9px;width:7px;margin-top:-9px;margin-right:5px;border-bottom-left-radius:1px;border-left:2px solid #ddd;border-bottom:2px solid #ddd}.tabulator-row .tabulator-cell .tabulator-data-tree-control{display:inline-flex;justify-content:center;align-items:center;vertical-align:middle;height:11px;width:11px;margin-right:5px;border:1px solid #333;border-radius:2px;background:rgba(0,0,0,.1);overflow:hidden}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-cell .tabulator-data-tree-control:hover{cursor:pointer;background:rgba(0,0,0,.2)}}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-collapse{display:inline-block;position:relative;height:7px;width:1px;background:transparent}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-collapse:after{position:absolute;content:\\\"\\\";left:-3px;top:3px;height:1px;width:7px;background:#333}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-expand{display:inline-block;position:relative;height:7px;width:1px;background:#333}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-expand:after{position:absolute;content:\\\"\\\";left:-3px;top:3px;height:1px;width:7px;background:#333}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle{display:inline-flex;align-items:center;justify-content:center;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none;height:15px;width:15px;border-radius:20px;background:#666;color:#fff;font-weight:700;font-size:1.1em}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle:hover{opacity:.7;cursor:pointer}}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle.open .tabulator-responsive-collapse-toggle-close{display:initial}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle.open .tabulator-responsive-collapse-toggle-open{display:none}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle svg{stroke:#fff}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle .tabulator-responsive-collapse-toggle-close{display:none}.tabulator-row .tabulator-cell .tabulator-traffic-light{display:inline-block;height:14px;width:14px;border-radius:14px}.tabulator-row.tabulator-group{box-sizing:border-box;border-bottom:1px solid #999;border-right:1px solid #ddd;border-top:1px solid #999;padding:5px 5px 5px 10px;background:#ccc;font-weight:700;min-width:100%}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-group:hover{cursor:pointer;background-color:rgba(0,0,0,.1)}}.tabulator-row.tabulator-group.tabulator-group-visible .tabulator-arrow{margin-right:10px;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid #666;border-bottom:0}.tabulator-row.tabulator-group.tabulator-group-level-1{padding-left:30px}.tabulator-row.tabulator-group.tabulator-group-level-2{padding-left:50px}.tabulator-row.tabulator-group.tabulator-group-level-3{padding-left:70px}.tabulator-row.tabulator-group.tabulator-group-level-4{padding-left:90px}.tabulator-row.tabulator-group.tabulator-group-level-5{padding-left:110px}.tabulator-row.tabulator-group .tabulator-group-toggle{display:inline-block}.tabulator-row.tabulator-group .tabulator-arrow{display:inline-block;width:0;height:0;margin-right:16px;border-top:6px solid transparent;border-bottom:6px solid transparent;border-right:0;border-left:6px solid #666;vertical-align:middle}.tabulator-row.tabulator-group span{margin-left:10px;color:#d00}.tabulator-popup-container{position:absolute;display:inline-block;box-sizing:border-box;background:#fff;border:1px solid #ddd;box-shadow:0 0 5px 0 rgba(0,0,0,.2);font-size:14px;overflow-y:auto;-webkit-overflow-scrolling:touch;z-index:10000}.tabulator-popup{padding:5px;border-radius:3px}.tabulator-tooltip{max-width:Min(500px,100%);padding:3px 5px;border-radius:2px;box-shadow:none;font-size:12px;pointer-events:none}.tabulator-menu .tabulator-menu-item{position:relative;box-sizing:border-box;padding:5px 10px;user-select:none}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-disabled{opacity:.5}@media (hover:hover) and (pointer:fine){.tabulator-menu .tabulator-menu-item:not(.tabulator-menu-item-disabled):hover{cursor:pointer;background:#fff}}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-submenu{padding-right:25px}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-submenu:after{display:inline-block;position:absolute;top:calc(5px + .4em);right:10px;height:7px;width:7px;content:\\\"\\\";border-color:#ddd;border-style:solid;border-width:1px 1px 0 0;vertical-align:top;transform:rotate(45deg)}.tabulator-menu .tabulator-menu-separator{border-top:1px solid #ddd}.tabulator-edit-list{max-height:200px;font-size:14px;overflow-y:auto;-webkit-overflow-scrolling:touch}.tabulator-edit-list .tabulator-edit-list-item{padding:4px;color:#333;outline:none}.tabulator-edit-list .tabulator-edit-list-item.active{color:#fff;background:#1d68cd}.tabulator-edit-list .tabulator-edit-list-item.active.focused{outline:1px solid hsla(0,0%,100%,.5)}.tabulator-edit-list .tabulator-edit-list-item.focused{outline:1px solid #1d68cd}@media (hover:hover) and (pointer:fine){.tabulator-edit-list .tabulator-edit-list-item:hover{cursor:pointer;color:#fff;background:#1d68cd}}.tabulator-edit-list .tabulator-edit-list-placeholder{padding:4px;color:#333;text-align:center}.tabulator-edit-list .tabulator-edit-list-group{border-bottom:1px solid #ddd;padding:6px 4px 4px;color:#333;font-weight:700}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-2,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-2{padding-left:12px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-3,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-3{padding-left:20px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-4,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-4{padding-left:28px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-5,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-5{padding-left:36px}.tabulator.tabulator-ltr{direction:ltr}.tabulator.tabulator-rtl{text-align:initial;direction:rtl}.tabulator.tabulator-rtl .tabulator-header .tabulator-col{text-align:initial;border-left:1px solid #ddd;border-right:initial}.tabulator.tabulator-rtl .tabulator-header .tabulator-col.tabulator-col-group .tabulator-col-group-cols{margin-right:0;margin-left:-1px}.tabulator.tabulator-rtl .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title{padding-right:0;padding-left:25px}.tabulator.tabulator-rtl .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter{left:8px;right:auto}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell{border-right:initial;border-left:1px solid #ddd}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell .tabulator-data-tree-branch{margin-right:0;margin-left:5px;border-bottom-left-radius:0;border-bottom-right-radius:1px;border-left:initial;border-right:2px solid #ddd}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell .tabulator-data-tree-control{margin-right:0;margin-left:5px}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left{border-left:2px solid #ddd}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right{border-right:2px solid #ddd}.tabulator.tabulator-rtl .tabulator-row .tabulator-col-resize-handle:last-of-type{width:3px;margin-left:0;margin-right:-3px}.tabulator.tabulator-rtl .tabulator-footer .tabulator-calcs-holder{text-align:initial}.tabulator-print-fullscreen{position:absolute;top:0;bottom:0;left:0;right:0;z-index:10000}body.tabulator-print-fullscreen-hide>:not(.tabulator-print-fullscreen){display:none!important}.tabulator-print-table{border-collapse:collapse}.tabulator-print-table .tabulator-data-tree-branch{display:inline-block;vertical-align:middle;height:9px;width:7px;margin-top:-9px;margin-right:5px;border-bottom-left-radius:1px;border-left:2px solid #ddd;border-bottom:2px solid #ddd}.tabulator-print-table .tabulator-print-table-group{box-sizing:border-box;border-bottom:1px solid #999;border-right:1px solid #ddd;border-top:1px solid #999;padding:5px 5px 5px 10px;background:#ccc;font-weight:700;min-width:100%}@media (hover:hover) and (pointer:fine){.tabulator-print-table .tabulator-print-table-group:hover{cursor:pointer;background-color:rgba(0,0,0,.1)}}.tabulator-print-table .tabulator-print-table-group.tabulator-group-visible .tabulator-arrow{margin-right:10px;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid #666;border-bottom:0}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-1 td{padding-left:30px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-2 td{padding-left:50px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-3 td{padding-left:70px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-4 td{padding-left:90px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-5 td{padding-left:110px!important}.tabulator-print-table .tabulator-print-table-group .tabulator-group-toggle{display:inline-block}.tabulator-print-table .tabulator-print-table-group .tabulator-arrow{display:inline-block;width:0;height:0;margin-right:16px;border-top:6px solid transparent;border-bottom:6px solid transparent;border-right:0;border-left:6px solid #666;vertical-align:middle}.tabulator-print-table .tabulator-print-table-group span{color:#d00}.tabulator-print-table .tabulator-data-tree-control{display:inline-flex;justify-content:center;align-items:center;vertical-align:middle;height:11px;width:11px;margin-right:5px;border:1px solid #333;border-radius:2px;background:rgba(0,0,0,.1);overflow:hidden}@media (hover:hover) and (pointer:fine){.tabulator-print-table .tabulator-data-tree-control:hover{cursor:pointer;background:rgba(0,0,0,.2)}}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-collapse{display:inline-block;position:relative;height:7px;width:1px;background:transparent}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-collapse:after{position:absolute;content:\\\"\\\";left:-3px;top:3px;height:1px;width:7px;background:#333}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-expand{display:inline-block;position:relative;height:7px;width:1px;background:#333}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-expand:after{position:absolute;content:\\\"\\\";left:-3px;top:3px;height:1px;width:7px;background:#333}.tabulator{border:none;background-color:#fff}.tabulator .tabulator-header .tabulator-calcs-holder{background:#f2f2f2!important;border-bottom:1px solid #999}.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row{background:#f2f2f2!important}.tabulator .tabulator-tableholder .tabulator-placeholder span{color:#000}.tabulator .tabulator-footer .tabulator-calcs-holder{background:#f2f2f2!important;border-bottom:1px solid #fff}.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row{background:#f2f2f2!important}.tabulator-row{border-bottom:1px solid #ddd}.tabulator-row .tabulator-cell:last-of-type{border-right:none}.tabulator-row.tabulator-group span{color:#666}.tabulator-print-table .tabulator-print-table-group span{margin-left:10px;color:#666}\\n/*# sourceMappingURL=tabulator_simple.min.css.map */\");\n }, function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (var i = 0; i < inline_js.length; i++) {\n inline_js[i].call(root, root.Bokeh);\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n\tvar NewBokeh = root.Bokeh;\n\tif (Bokeh.versions === undefined) {\n\t Bokeh.versions = new Map();\n\t}\n\tif (NewBokeh.version !== Bokeh.version) {\n\t Bokeh.versions.set(NewBokeh.version, NewBokeh)\n\t}\n\troot.Bokeh = Bokeh;\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\n\troot.Bokeh = undefined;\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n\tconsole.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n\trun_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n comm.open();\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n }) \n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n var force = true;\n var py_version = '3.1.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n var is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n run_callbacks();\n return null;\n }\n if (!reloading) {\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error() {\n console.error(\"failed to load \" + url);\n }\n\n var skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {'tabulator': 'https://cdn.jsdelivr.net/npm/tabulator-tables@5.5.0/dist/js/tabulator', 'moment': 'https://cdn.jsdelivr.net/npm/luxon/build/global/luxon.min', 'mathjax': '//cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS_HTML', 'vega-embed': 'https://cdn.jsdelivr.net/npm/vega-embed@6/build/vega-embed.min', 'vega-lite': 'https://cdn.jsdelivr.net/npm/vega-lite@5/build/vega-lite.min', 'vega': 'https://cdn.jsdelivr.net/npm/vega@5/build/vega.min', 'jspanel': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel', 'jspanel-modal': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal', 'jspanel-tooltip': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip', 'jspanel-hint': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint', 'jspanel-layout': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout', 'jspanel-contextmenu': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu', 'jspanel-dock': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock', 'gridstack': 'https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack-all', 'notyf': 'https://cdn.jsdelivr.net/npm/notyf@3/notyf.min'}, 'shim': {'mathjax': {'exports': 'MathJax'}, 'jspanel': {'exports': 'jsPanel'}, 'gridstack': {'exports': 'GridStack'}}});\n require([\"tabulator\"], function(Tabulator) {\n\twindow.Tabulator = Tabulator\n\ton_load()\n })\n require([\"moment\"], function(moment) {\n\twindow.moment = moment\n\ton_load()\n })\n require([\"mathjax\"], function() {\n\ton_load()\n })\n require([\"vega-embed\"], function(vegaEmbed) {\n\twindow.vegaEmbed = vegaEmbed\n\ton_load()\n })\n require([\"vega-lite\"], function(vl) {\n\twindow.vl = vl\n\ton_load()\n })\n require([\"vega\"], function(vega) {\n\twindow.vega = vega\n\ton_load()\n })\n require([\"jspanel\"], function(jsPanel) {\n\twindow.jsPanel = jsPanel\n\ton_load()\n })\n require([\"jspanel-modal\"], function() {\n\ton_load()\n })\n require([\"jspanel-tooltip\"], function() {\n\ton_load()\n })\n require([\"jspanel-hint\"], function() {\n\ton_load()\n })\n require([\"jspanel-layout\"], function() {\n\ton_load()\n })\n require([\"jspanel-contextmenu\"], function() {\n\ton_load()\n })\n require([\"jspanel-dock\"], function() {\n\ton_load()\n })\n require([\"gridstack\"], function(GridStack) {\n\twindow.GridStack = GridStack\n\ton_load()\n })\n require([\"notyf\"], function() {\n\ton_load()\n })\n root._bokeh_is_loading = css_urls.length + 15;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n var existing_stylesheets = []\n var links = document.getElementsByTagName('link')\n for (var i = 0; i < links.length; i++) {\n var link = links[i]\n if (link.href != null) {\n\texisting_stylesheets.push(link.href)\n }\n }\n for (var i = 0; i < css_urls.length; i++) {\n var url = css_urls[i];\n if (existing_stylesheets.indexOf(url) !== -1) {\n\ton_load()\n\tcontinue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } if (((window['Tabulator'] !== undefined) && (!(window['Tabulator'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['moment'] !== undefined) && (!(window['moment'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/datatabulator/luxon/build/global/luxon.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['MathJax'] !== undefined) && (!(window['MathJax'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['vega'] !== undefined) && (!(window['vega'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/vegaplot/vega@5'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['vegaLite'] !== undefined) && (!(window['vegaLite'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/vegaplot/vega-lite@5'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['vegaEmbed'] !== undefined) && (!(window['vegaEmbed'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/vegaplot/vega-embed@6'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['jsPanel'] !== undefined) && (!(window['jsPanel'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.2.3/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['GridStack'] !== undefined) && (!(window['GridStack'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/gridstack/gridstack@7.2.3/dist/gridstack-all.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['Notyf'] !== undefined) && (!(window['Notyf'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.3/dist/bundled/notificationarea/notyf@3/notyf.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } var existing_scripts = []\n var scripts = document.getElementsByTagName('script')\n for (var i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n\texisting_scripts.push(script.src)\n }\n }\n for (var i = 0; i < js_urls.length; i++) {\n var url = js_urls[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (var i = 0; i < js_modules.length; i++) {\n var url = js_modules[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n var url = js_exports[name];\n if (skip.indexOf(url) >= 0 || root[name] != null) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n var js_urls = [\"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML\", \"https://cdn.holoviz.org/panel/1.2.3/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/js/tabulator.js\", \"https://cdn.holoviz.org/panel/1.2.3/dist/bundled/datatabulator/luxon/build/global/luxon.min.js\", \"https://cdn.holoviz.org/panel/1.2.3/dist/bundled/vegaplot/vega@5\", \"https://cdn.holoviz.org/panel/1.2.3/dist/bundled/vegaplot/vega-lite@5\", \"https://cdn.holoviz.org/panel/1.2.3/dist/bundled/vegaplot/vega-embed@6\"];\n var js_modules = [];\n var js_exports = {};\n var css_urls = [\"https://cdn.holoviz.org/panel/1.2.3/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_simple.min.css\"];\n var inline_js = [ function(Bokeh) {\n inject_raw_css(\".tabulator{position:relative;border:1px solid #999;font-size:14px;text-align:left;overflow:hidden;-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}.tabulator[tabulator-layout=fitDataFill] .tabulator-tableholder .tabulator-table{min-width:100%}.tabulator[tabulator-layout=fitDataTable]{display:inline-block}.tabulator.tabulator-block-select{user-select:none}.tabulator .tabulator-header{position:relative;box-sizing:border-box;width:100%;border-bottom:1px solid #999;background-color:#fff;color:#555;font-weight:700;white-space:nowrap;overflow:hidden;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none}.tabulator .tabulator-header.tabulator-header-hidden{display:none}.tabulator .tabulator-header .tabulator-header-contents{position:relative;overflow:hidden}.tabulator .tabulator-header .tabulator-header-contents .tabulator-headers{display:inline-block}.tabulator .tabulator-header .tabulator-col{display:inline-flex;position:relative;box-sizing:border-box;flex-direction:column;justify-content:flex-start;border-right:1px solid #ddd;background:#fff;text-align:left;vertical-align:bottom;overflow:hidden}.tabulator .tabulator-header .tabulator-col.tabulator-moving{position:absolute;border:1px solid #999;background:#e6e6e6;pointer-events:none}.tabulator .tabulator-header .tabulator-col .tabulator-col-content{box-sizing:border-box;position:relative;padding:4px}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-header-popup-button{padding:0 8px}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-header-popup-button:hover{cursor:pointer;opacity:.6}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title-holder{position:relative}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title{box-sizing:border-box;width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title.tabulator-col-title-wrap{white-space:normal;text-overflow:clip}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title .tabulator-title-editor{box-sizing:border-box;width:100%;border:1px solid #999;padding:1px;background:#fff}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title .tabulator-header-popup-button+.tabulator-title-editor{width:calc(100% - 22px)}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter{display:flex;align-items:center;position:absolute;top:0;bottom:0;right:4px}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #bbb}.tabulator .tabulator-header .tabulator-col.tabulator-col-group .tabulator-col-group-cols{position:relative;display:flex;border-top:1px solid #ddd;overflow:hidden;margin-right:-1px}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter{position:relative;box-sizing:border-box;margin-top:2px;width:100%;text-align:center}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter textarea{height:auto!important}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter svg{margin-top:3px}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter input::-ms-clear{width:0;height:0}.tabulator .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title{padding-right:25px}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover{cursor:pointer;background-color:#e6e6e6}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter{color:#bbb}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{cursor:pointer;border-bottom:6px solid #555}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-top:none;border-bottom:6px solid #bbb}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter{color:#666}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{cursor:pointer;border-bottom:6px solid #555}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-top:none;border-bottom:6px solid #666}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter{color:#666}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{cursor:pointer;border-top:6px solid #555}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-bottom:none;border-top:6px solid #666;color:#666}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical .tabulator-col-content .tabulator-col-title{writing-mode:vertical-rl;text-orientation:mixed;display:flex;align-items:center;justify-content:center}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-col-vertical-flip .tabulator-col-title{transform:rotate(180deg)}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable .tabulator-col-title{padding-right:0;padding-top:20px}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable.tabulator-col-vertical-flip .tabulator-col-title{padding-right:0;padding-bottom:20px}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable .tabulator-col-sorter{justify-content:center;left:0;right:0;top:4px;bottom:auto}.tabulator .tabulator-header .tabulator-frozen{position:sticky;left:0;z-index:10}.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left{border-right:2px solid #ddd}.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-right{border-left:2px solid #ddd}.tabulator .tabulator-header .tabulator-calcs-holder{box-sizing:border-box;background:#fff!important;border-top:1px solid #ddd;border-bottom:1px solid #ddd}.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row{background:#fff!important}.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row .tabulator-col-resize-handle,.tabulator .tabulator-header .tabulator-frozen-rows-holder:empty{display:none}.tabulator .tabulator-tableholder{position:relative;width:100%;white-space:nowrap;overflow:auto;-webkit-overflow-scrolling:touch}.tabulator .tabulator-tableholder:focus{outline:none}.tabulator .tabulator-tableholder .tabulator-placeholder{box-sizing:border-box;display:flex;align-items:center;justify-content:center;width:100%}.tabulator .tabulator-tableholder .tabulator-placeholder[tabulator-render-mode=virtual]{min-height:100%;min-width:100%}.tabulator .tabulator-tableholder .tabulator-placeholder .tabulator-placeholder-contents{display:inline-block;text-align:center;padding:10px;color:#ccc;font-weight:700;font-size:20px;white-space:normal}.tabulator .tabulator-tableholder .tabulator-table{position:relative;display:inline-block;background-color:#fff;white-space:nowrap;overflow:visible;color:#333}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs{font-weight:700;background:#f2f2f2!important}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs.tabulator-calcs-top{border-bottom:2px solid #ddd}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs.tabulator-calcs-bottom{border-top:2px solid #ddd}.tabulator .tabulator-footer{border-top:1px solid #999;background-color:#fff;color:#555;font-weight:700;white-space:nowrap;user-select:none;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none}.tabulator .tabulator-footer .tabulator-footer-contents{display:flex;flex-direction:row;align-items:center;justify-content:space-between;padding:5px 10px}.tabulator .tabulator-footer .tabulator-footer-contents:empty{display:none}.tabulator .tabulator-footer .tabulator-calcs-holder{box-sizing:border-box;width:100%;text-align:left;background:#fff!important;border-bottom:1px solid #ddd;border-top:1px solid #ddd;overflow:hidden}.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row{display:inline-block;background:#fff!important}.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row .tabulator-col-resize-handle{display:none}.tabulator .tabulator-footer .tabulator-calcs-holder:only-child{margin-bottom:-5px;border-bottom:none}.tabulator .tabulator-footer>*+.tabulator-page-counter{margin-left:10px}.tabulator .tabulator-footer .tabulator-page-counter{font-weight:400}.tabulator .tabulator-footer .tabulator-paginator{flex:1;text-align:right;color:#555;font-family:inherit;font-weight:inherit;font-size:inherit}.tabulator .tabulator-footer .tabulator-page-size{display:inline-block;margin:0 5px;padding:2px 5px;border:1px solid #aaa;border-radius:3px}.tabulator .tabulator-footer .tabulator-pages{margin:0 7px}.tabulator .tabulator-footer .tabulator-page{display:inline-block;margin:0 2px;padding:2px 5px;border:1px solid #aaa;border-radius:3px;background:hsla(0,0%,100%,.2)}.tabulator .tabulator-footer .tabulator-page.active{color:#d00}.tabulator .tabulator-footer .tabulator-page:disabled{opacity:.5}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-footer .tabulator-page:not(.disabled):hover{cursor:pointer;background:rgba(0,0,0,.2);color:#fff}}.tabulator .tabulator-col-resize-handle{position:relative;display:inline-block;width:6px;margin-left:-3px;margin-right:-3px;z-index:10;vertical-align:middle}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-col-resize-handle:hover{cursor:ew-resize}}.tabulator .tabulator-col-resize-handle:last-of-type{width:3px;margin-right:0}.tabulator .tabulator-alert{position:absolute;display:flex;align-items:center;top:0;left:0;z-index:100;height:100%;width:100%;background:rgba(0,0,0,.4);text-align:center}.tabulator .tabulator-alert .tabulator-alert-msg{display:inline-block;margin:0 auto;padding:10px 20px;border-radius:10px;background:#fff;font-weight:700;font-size:16px}.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-msg{border:4px solid #333;color:#000}.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-error{border:4px solid #d00;color:#590000}.tabulator-row{position:relative;box-sizing:border-box;min-height:22px}.tabulator-row,.tabulator-row.tabulator-row-even{background-color:#fff}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-selectable:hover{background-color:#bbb;cursor:pointer}}.tabulator-row.tabulator-selected{background-color:#9abcea}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-selected:hover{background-color:#769bcc;cursor:pointer}}.tabulator-row.tabulator-row-moving{border:1px solid #000;background:#fff}.tabulator-row.tabulator-moving{position:absolute;border-top:1px solid #ddd;border-bottom:1px solid #ddd;pointer-events:none;z-index:15}.tabulator-row .tabulator-row-resize-handle{position:absolute;right:0;bottom:0;left:0;height:5px}.tabulator-row .tabulator-row-resize-handle.prev{top:0;bottom:auto}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-row-resize-handle:hover{cursor:ns-resize}}.tabulator-row .tabulator-responsive-collapse{box-sizing:border-box;padding:5px;border-top:1px solid #ddd;border-bottom:1px solid #ddd}.tabulator-row .tabulator-responsive-collapse:empty{display:none}.tabulator-row .tabulator-responsive-collapse table{font-size:14px}.tabulator-row .tabulator-responsive-collapse table tr td{position:relative}.tabulator-row .tabulator-responsive-collapse table tr td:first-of-type{padding-right:10px}.tabulator-row .tabulator-cell{display:inline-block;position:relative;box-sizing:border-box;padding:4px;border-right:1px solid #ddd;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tabulator-row .tabulator-cell.tabulator-frozen{display:inline-block;position:sticky;left:0;background-color:inherit;z-index:10}.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left{border-right:2px solid #ddd}.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right{border-left:2px solid #ddd}.tabulator-row .tabulator-cell.tabulator-editing{border:1px solid #1d68cd;outline:none;padding:0}.tabulator-row .tabulator-cell.tabulator-editing input,.tabulator-row .tabulator-cell.tabulator-editing select{border:1px;background:transparent;outline:none}.tabulator-row .tabulator-cell.tabulator-validation-fail{border:1px solid #d00}.tabulator-row .tabulator-cell.tabulator-validation-fail input,.tabulator-row .tabulator-cell.tabulator-validation-fail select{border:1px;background:transparent;color:#d00}.tabulator-row .tabulator-cell.tabulator-row-handle{display:inline-flex;align-items:center;justify-content:center;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none}.tabulator-row .tabulator-cell.tabulator-row-handle .tabulator-row-handle-box{width:80%}.tabulator-row .tabulator-cell.tabulator-row-handle .tabulator-row-handle-box .tabulator-row-handle-bar{width:100%;height:3px;margin-top:2px;background:#666}.tabulator-row .tabulator-cell .tabulator-data-tree-branch{display:inline-block;vertical-align:middle;height:9px;width:7px;margin-top:-9px;margin-right:5px;border-bottom-left-radius:1px;border-left:2px solid #ddd;border-bottom:2px solid #ddd}.tabulator-row .tabulator-cell .tabulator-data-tree-control{display:inline-flex;justify-content:center;align-items:center;vertical-align:middle;height:11px;width:11px;margin-right:5px;border:1px solid #333;border-radius:2px;background:rgba(0,0,0,.1);overflow:hidden}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-cell .tabulator-data-tree-control:hover{cursor:pointer;background:rgba(0,0,0,.2)}}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-collapse{display:inline-block;position:relative;height:7px;width:1px;background:transparent}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-collapse:after{position:absolute;content:\\\"\\\";left:-3px;top:3px;height:1px;width:7px;background:#333}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-expand{display:inline-block;position:relative;height:7px;width:1px;background:#333}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-expand:after{position:absolute;content:\\\"\\\";left:-3px;top:3px;height:1px;width:7px;background:#333}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle{display:inline-flex;align-items:center;justify-content:center;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none;height:15px;width:15px;border-radius:20px;background:#666;color:#fff;font-weight:700;font-size:1.1em}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle:hover{opacity:.7;cursor:pointer}}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle.open .tabulator-responsive-collapse-toggle-close{display:initial}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle.open .tabulator-responsive-collapse-toggle-open{display:none}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle svg{stroke:#fff}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle .tabulator-responsive-collapse-toggle-close{display:none}.tabulator-row .tabulator-cell .tabulator-traffic-light{display:inline-block;height:14px;width:14px;border-radius:14px}.tabulator-row.tabulator-group{box-sizing:border-box;border-bottom:1px solid #999;border-right:1px solid #ddd;border-top:1px solid #999;padding:5px 5px 5px 10px;background:#ccc;font-weight:700;min-width:100%}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-group:hover{cursor:pointer;background-color:rgba(0,0,0,.1)}}.tabulator-row.tabulator-group.tabulator-group-visible .tabulator-arrow{margin-right:10px;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid #666;border-bottom:0}.tabulator-row.tabulator-group.tabulator-group-level-1{padding-left:30px}.tabulator-row.tabulator-group.tabulator-group-level-2{padding-left:50px}.tabulator-row.tabulator-group.tabulator-group-level-3{padding-left:70px}.tabulator-row.tabulator-group.tabulator-group-level-4{padding-left:90px}.tabulator-row.tabulator-group.tabulator-group-level-5{padding-left:110px}.tabulator-row.tabulator-group .tabulator-group-toggle{display:inline-block}.tabulator-row.tabulator-group .tabulator-arrow{display:inline-block;width:0;height:0;margin-right:16px;border-top:6px solid transparent;border-bottom:6px solid transparent;border-right:0;border-left:6px solid #666;vertical-align:middle}.tabulator-row.tabulator-group span{margin-left:10px;color:#d00}.tabulator-popup-container{position:absolute;display:inline-block;box-sizing:border-box;background:#fff;border:1px solid #ddd;box-shadow:0 0 5px 0 rgba(0,0,0,.2);font-size:14px;overflow-y:auto;-webkit-overflow-scrolling:touch;z-index:10000}.tabulator-popup{padding:5px;border-radius:3px}.tabulator-tooltip{max-width:Min(500px,100%);padding:3px 5px;border-radius:2px;box-shadow:none;font-size:12px;pointer-events:none}.tabulator-menu .tabulator-menu-item{position:relative;box-sizing:border-box;padding:5px 10px;user-select:none}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-disabled{opacity:.5}@media (hover:hover) and (pointer:fine){.tabulator-menu .tabulator-menu-item:not(.tabulator-menu-item-disabled):hover{cursor:pointer;background:#fff}}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-submenu{padding-right:25px}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-submenu:after{display:inline-block;position:absolute;top:calc(5px + .4em);right:10px;height:7px;width:7px;content:\\\"\\\";border-color:#ddd;border-style:solid;border-width:1px 1px 0 0;vertical-align:top;transform:rotate(45deg)}.tabulator-menu .tabulator-menu-separator{border-top:1px solid #ddd}.tabulator-edit-list{max-height:200px;font-size:14px;overflow-y:auto;-webkit-overflow-scrolling:touch}.tabulator-edit-list .tabulator-edit-list-item{padding:4px;color:#333;outline:none}.tabulator-edit-list .tabulator-edit-list-item.active{color:#fff;background:#1d68cd}.tabulator-edit-list .tabulator-edit-list-item.active.focused{outline:1px solid hsla(0,0%,100%,.5)}.tabulator-edit-list .tabulator-edit-list-item.focused{outline:1px solid #1d68cd}@media (hover:hover) and (pointer:fine){.tabulator-edit-list .tabulator-edit-list-item:hover{cursor:pointer;color:#fff;background:#1d68cd}}.tabulator-edit-list .tabulator-edit-list-placeholder{padding:4px;color:#333;text-align:center}.tabulator-edit-list .tabulator-edit-list-group{border-bottom:1px solid #ddd;padding:6px 4px 4px;color:#333;font-weight:700}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-2,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-2{padding-left:12px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-3,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-3{padding-left:20px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-4,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-4{padding-left:28px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-5,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-5{padding-left:36px}.tabulator.tabulator-ltr{direction:ltr}.tabulator.tabulator-rtl{text-align:initial;direction:rtl}.tabulator.tabulator-rtl .tabulator-header .tabulator-col{text-align:initial;border-left:1px solid #ddd;border-right:initial}.tabulator.tabulator-rtl .tabulator-header .tabulator-col.tabulator-col-group .tabulator-col-group-cols{margin-right:0;margin-left:-1px}.tabulator.tabulator-rtl .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title{padding-right:0;padding-left:25px}.tabulator.tabulator-rtl .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter{left:8px;right:auto}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell{border-right:initial;border-left:1px solid #ddd}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell .tabulator-data-tree-branch{margin-right:0;margin-left:5px;border-bottom-left-radius:0;border-bottom-right-radius:1px;border-left:initial;border-right:2px solid #ddd}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell .tabulator-data-tree-control{margin-right:0;margin-left:5px}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left{border-left:2px solid #ddd}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right{border-right:2px solid #ddd}.tabulator.tabulator-rtl .tabulator-row .tabulator-col-resize-handle:last-of-type{width:3px;margin-left:0;margin-right:-3px}.tabulator.tabulator-rtl .tabulator-footer .tabulator-calcs-holder{text-align:initial}.tabulator-print-fullscreen{position:absolute;top:0;bottom:0;left:0;right:0;z-index:10000}body.tabulator-print-fullscreen-hide>:not(.tabulator-print-fullscreen){display:none!important}.tabulator-print-table{border-collapse:collapse}.tabulator-print-table .tabulator-data-tree-branch{display:inline-block;vertical-align:middle;height:9px;width:7px;margin-top:-9px;margin-right:5px;border-bottom-left-radius:1px;border-left:2px solid #ddd;border-bottom:2px solid #ddd}.tabulator-print-table .tabulator-print-table-group{box-sizing:border-box;border-bottom:1px solid #999;border-right:1px solid #ddd;border-top:1px solid #999;padding:5px 5px 5px 10px;background:#ccc;font-weight:700;min-width:100%}@media (hover:hover) and (pointer:fine){.tabulator-print-table .tabulator-print-table-group:hover{cursor:pointer;background-color:rgba(0,0,0,.1)}}.tabulator-print-table .tabulator-print-table-group.tabulator-group-visible .tabulator-arrow{margin-right:10px;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid #666;border-bottom:0}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-1 td{padding-left:30px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-2 td{padding-left:50px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-3 td{padding-left:70px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-4 td{padding-left:90px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-5 td{padding-left:110px!important}.tabulator-print-table .tabulator-print-table-group .tabulator-group-toggle{display:inline-block}.tabulator-print-table .tabulator-print-table-group .tabulator-arrow{display:inline-block;width:0;height:0;margin-right:16px;border-top:6px solid transparent;border-bottom:6px solid transparent;border-right:0;border-left:6px solid #666;vertical-align:middle}.tabulator-print-table .tabulator-print-table-group span{color:#d00}.tabulator-print-table .tabulator-data-tree-control{display:inline-flex;justify-content:center;align-items:center;vertical-align:middle;height:11px;width:11px;margin-right:5px;border:1px solid #333;border-radius:2px;background:rgba(0,0,0,.1);overflow:hidden}@media (hover:hover) and (pointer:fine){.tabulator-print-table .tabulator-data-tree-control:hover{cursor:pointer;background:rgba(0,0,0,.2)}}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-collapse{display:inline-block;position:relative;height:7px;width:1px;background:transparent}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-collapse:after{position:absolute;content:\\\"\\\";left:-3px;top:3px;height:1px;width:7px;background:#333}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-expand{display:inline-block;position:relative;height:7px;width:1px;background:#333}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-expand:after{position:absolute;content:\\\"\\\";left:-3px;top:3px;height:1px;width:7px;background:#333}.tabulator{border:none;background-color:#fff}.tabulator .tabulator-header .tabulator-calcs-holder{background:#f2f2f2!important;border-bottom:1px solid #999}.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row{background:#f2f2f2!important}.tabulator .tabulator-tableholder .tabulator-placeholder span{color:#000}.tabulator .tabulator-footer .tabulator-calcs-holder{background:#f2f2f2!important;border-bottom:1px solid #fff}.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row{background:#f2f2f2!important}.tabulator-row{border-bottom:1px solid #ddd}.tabulator-row .tabulator-cell:last-of-type{border-right:none}.tabulator-row.tabulator-group span{color:#666}.tabulator-print-table .tabulator-print-table-group span{margin-left:10px;color:#666}\\n/*# sourceMappingURL=tabulator_simple.min.css.map */\");\n }, function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (var i = 0; i < inline_js.length; i++) {\n inline_js[i].call(root, root.Bokeh);\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n\tvar NewBokeh = root.Bokeh;\n\tif (Bokeh.versions === undefined) {\n\t Bokeh.versions = new Map();\n\t}\n\tif (NewBokeh.version !== Bokeh.version) {\n\t Bokeh.versions.set(NewBokeh.version, NewBokeh)\n\t}\n\troot.Bokeh = Bokeh;\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\n\troot.Bokeh = undefined;\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n\tconsole.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n\trun_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n comm.open();\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n }) \n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import bdikit as bdi\n", + "import pandas as pd\n", + "from bdikit.utils import get_gdc_data\n", + "from pprint import pprint\n", + "\n", + "import flair, torch\n", + "flair.device = torch.device('cpu') " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
CountryHistologic_Grade_FIGOHistologic_typePath_Stage_Primary_Tumor-pTPath_Stage_Reg_Lymph_Nodes-pNClin_Stage_Dist_Mets-cMPath_Stage_Dist_Mets-pMtumor_Stage-PathologicalFIGO_stageBMIAgeRaceEthnicityGenderTumor_SiteTumor_FocalityTumor_Size_cm
0United StatesFIGO grade 1EndometrioidpT1a (FIGO IA)pN0cM0Staging IncompleteStage IIA38.8864.0WhiteNot-Hispanic or LatinoFemaleAnterior endometriumUnifocal2.9
1United StatesFIGO grade 1EndometrioidpT1a (FIGO IA)pNXcM0Staging IncompleteStage IVIA39.7658.0WhiteNot-Hispanic or LatinoFemalePosterior endometriumUnifocal3.5
2United StatesFIGO grade 2EndometrioidpT1a (FIGO IA)pN0cM0Staging IncompleteStage IIA51.1950.0WhiteNot-Hispanic or LatinoFemaleOther, specifyUnifocal4.5
3NaNNaNCarcinosarcomaNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
4United StatesFIGO grade 2EndometrioidpT1a (FIGO IA)pNXcM0No pathologic evidence of distant metastasisStage IIA32.6975.0WhiteNot-Hispanic or LatinoFemaleOther, specifyUnifocal3.5
\n", + "
" + ], + "text/plain": [ + " Country Histologic_Grade_FIGO Histologic_type \\\n", + "0 United States FIGO grade 1 Endometrioid \n", + "1 United States FIGO grade 1 Endometrioid \n", + "2 United States FIGO grade 2 Endometrioid \n", + "3 NaN NaN Carcinosarcoma \n", + "4 United States FIGO grade 2 Endometrioid \n", + "\n", + " Path_Stage_Primary_Tumor-pT Path_Stage_Reg_Lymph_Nodes-pN \\\n", + "0 pT1a (FIGO IA) pN0 \n", + "1 pT1a (FIGO IA) pNX \n", + "2 pT1a (FIGO IA) pN0 \n", + "3 NaN NaN \n", + "4 pT1a (FIGO IA) pNX \n", + "\n", + " Clin_Stage_Dist_Mets-cM Path_Stage_Dist_Mets-pM \\\n", + "0 cM0 Staging Incomplete \n", + "1 cM0 Staging Incomplete \n", + "2 cM0 Staging Incomplete \n", + "3 NaN NaN \n", + "4 cM0 No pathologic evidence of distant metastasis \n", + "\n", + " tumor_Stage-Pathological FIGO_stage BMI Age Race \\\n", + "0 Stage I IA 38.88 64.0 White \n", + "1 Stage IV IA 39.76 58.0 White \n", + "2 Stage I IA 51.19 50.0 White \n", + "3 NaN NaN NaN NaN NaN \n", + "4 Stage I IA 32.69 75.0 White \n", + "\n", + " Ethnicity Gender Tumor_Site Tumor_Focality \\\n", + "0 Not-Hispanic or Latino Female Anterior endometrium Unifocal \n", + "1 Not-Hispanic or Latino Female Posterior endometrium Unifocal \n", + "2 Not-Hispanic or Latino Female Other, specify Unifocal \n", + "3 NaN NaN NaN NaN \n", + "4 Not-Hispanic or Latino Female Other, specify Unifocal \n", + "\n", + " Tumor_Size_cm \n", + "0 2.9 \n", + "1 3.5 \n", + "2 4.5 \n", + "3 NaN \n", + "4 3.5 " + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset = pd.read_csv('./datasets/dou.csv')\n", + "dataset.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Some weights of RobertaModel were not initialized from the model checkpoint at roberta-base and are newly initialized: ['roberta.pooler.dense.bias', 'roberta.pooler.dense.weight']\n", + "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n", + " 0%| | 0/17 [00:00\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcetarget
0Countrycountry_of_birth
1Histologic_Grade_FIGOhistologic_progression_type
2Histologic_typedysplasia_type
3Path_Stage_Primary_Tumor-pTuicc_clinical_m
4Path_Stage_Reg_Lymph_Nodes-pNfigo_stage
5Clin_Stage_Dist_Mets-cMinrg_stage
6Path_Stage_Dist_Mets-pMlast_known_disease_status
7tumor_Stage-Pathologicaltumor_grade_category
8FIGO_stagefigo_stage
9BMIhpv_positive_type
10Ageweight
11Racerace
12Ethnicityethnicity
13Gendergender
14Tumor_Sitetumor_shape
15Tumor_Focalitytumor_focality
16Tumor_Size_cmtumor_depth
\n", + "" + ], + "text/plain": [ + " source target\n", + "0 Country country_of_birth\n", + "1 Histologic_Grade_FIGO histologic_progression_type\n", + "2 Histologic_type dysplasia_type\n", + "3 Path_Stage_Primary_Tumor-pT uicc_clinical_m\n", + "4 Path_Stage_Reg_Lymph_Nodes-pN figo_stage\n", + "5 Clin_Stage_Dist_Mets-cM inrg_stage\n", + "6 Path_Stage_Dist_Mets-pM last_known_disease_status\n", + "7 tumor_Stage-Pathological tumor_grade_category\n", + "8 FIGO_stage figo_stage\n", + "9 BMI hpv_positive_type\n", + "10 Age weight\n", + "11 Race race\n", + "12 Ethnicity ethnicity\n", + "13 Gender gender\n", + "14 Tumor_Site tumor_shape\n", + "15 Tumor_Focality tumor_focality\n", + "16 Tumor_Size_cm tumor_depth" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "column_mappings = bdi.match_columns(dataset, target='gdc', method='two_phase')\n", + "column_mappings" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Some weights of RobertaModel were not initialized from the model checkpoint at roberta-base and are newly initialized: ['roberta.pooler.dense.bias', 'roberta.pooler.dense.weight']\n", + "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n", + "100%|██████████| 1/1 [00:00<00:00, 13.80it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Table features extracted from 1 columns\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 734/734 [00:48<00:00, 15.14it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Table features extracted from 734 columns\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcematchessimilarity
0BMInecrosis_percent0.7141
1BMIrecist_targeted_regions_sum0.7075
2BMIpercent_stromal_cells0.6953
3BMIspindle_cell_percent0.686
4BMIpercent_neutrophil_infiltration0.6063
5BMIfragment_standard_deviation_length0.593
6BMIlongest_dimension0.5782
7BMIintermediate_dimension0.5464
8BMIaverage_base_quality0.5251
9BMIbmi0.5218
\n", + "
" + ], + "text/plain": [ + " source matches similarity\n", + "0 BMI necrosis_percent 0.7141\n", + "1 BMI recist_targeted_regions_sum 0.7075\n", + "2 BMI percent_stromal_cells 0.6953\n", + "3 BMI spindle_cell_percent 0.686\n", + "4 BMI percent_neutrophil_infiltration 0.6063\n", + "5 BMI fragment_standard_deviation_length 0.593\n", + "6 BMI longest_dimension 0.5782\n", + "7 BMI intermediate_dimension 0.5464\n", + "8 BMI average_base_quality 0.5251\n", + "9 BMI bmi 0.5218" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bdi.top_matches(dataset, columns=['BMI'], target='gdc')" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Some weights of RobertaModel were not initialized from the model checkpoint at roberta-base and are newly initialized: ['roberta.pooler.dense.bias', 'roberta.pooler.dense.weight']\n", + "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n", + "100%|██████████| 1/1 [00:00<00:00, 13.58it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Table features extracted from 1 columns\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 734/734 [00:45<00:00, 15.98it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Table features extracted from 734 columns\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcematchessimilarity
0Ageepithelioid_cell_percent0.6855
1Agefev1_fvc_post_bronch_percent0.6439
2Agefev1_fvc_pre_bronch_percent0.642
3Agepercent_tumor_invasion0.6393
4Agepercent_necrosis0.5937
5Agepercent_tumor_nuclei0.5624
6Agepercent_inflam_infiltration0.5255
7Ageicd_10_code0.5052
8Ageage_at_last_exposure0.4992
9Agepercent_normal_cells0.4867
\n", + "
" + ], + "text/plain": [ + " source matches similarity\n", + "0 Age epithelioid_cell_percent 0.6855\n", + "1 Age fev1_fvc_post_bronch_percent 0.6439\n", + "2 Age fev1_fvc_pre_bronch_percent 0.642\n", + "3 Age percent_tumor_invasion 0.6393\n", + "4 Age percent_necrosis 0.5937\n", + "5 Age percent_tumor_nuclei 0.5624\n", + "6 Age percent_inflam_infiltration 0.5255\n", + "7 Age icd_10_code 0.5052\n", + "8 Age age_at_last_exposure 0.4992\n", + "9 Age percent_normal_cells 0.4867" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bdi.top_matches(dataset, columns=['Age'], target='gdc')" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Some weights of RobertaModel were not initialized from the model checkpoint at roberta-base and are newly initialized: ['roberta.pooler.dense.bias', 'roberta.pooler.dense.weight']\n", + "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n", + "100%|██████████| 1/1 [00:00<00:00, 13.62it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Table features extracted from 1 columns\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 734/734 [00:45<00:00, 16.17it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Table features extracted from 734 columns\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcematchessimilarity
0Tumor_Size_cmshortest_dimension0.7114
1Tumor_Size_cmsize_extraocular_nodule0.6994
2Tumor_Size_cmtumor_depth_measurement0.6734
3Tumor_Size_cmtumor_width_measurement0.6553
4Tumor_Size_cmanalyte_quantity0.634
5Tumor_Size_cmtumor_thickness0.6234
6Tumor_Size_cmaverage_insert_size0.6005
7Tumor_Size_cmmitotic_total_area0.5605
8Tumor_Size_cmrin0.5513
9Tumor_Size_cmimaging_suv0.5378
\n", + "
" + ], + "text/plain": [ + " source matches similarity\n", + "0 Tumor_Size_cm shortest_dimension 0.7114\n", + "1 Tumor_Size_cm size_extraocular_nodule 0.6994\n", + "2 Tumor_Size_cm tumor_depth_measurement 0.6734\n", + "3 Tumor_Size_cm tumor_width_measurement 0.6553\n", + "4 Tumor_Size_cm analyte_quantity 0.634\n", + "5 Tumor_Size_cm tumor_thickness 0.6234\n", + "6 Tumor_Size_cm average_insert_size 0.6005\n", + "7 Tumor_Size_cm mitotic_total_area 0.5605\n", + "8 Tumor_Size_cm rin 0.5513\n", + "9 Tumor_Size_cm imaging_suv 0.5378" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bdi.top_matches(dataset, columns=['Tumor_Size_cm'], target='gdc')" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Some weights of RobertaModel were not initialized from the model checkpoint at roberta-base and are newly initialized: ['roberta.pooler.dense.bias', 'roberta.pooler.dense.weight']\n", + "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n", + "100%|██████████| 1/1 [00:00<00:00, 21.02it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Table features extracted from 1 columns\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 734/734 [00:43<00:00, 16.84it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Table features extracted from 734 columns\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcematchessimilarity
0Tumor_Focalitytumor_focality0.6059
1Tumor_Focalitytumor_depth_descriptor0.5419
2Tumor_Focalitytumor_shape0.4857
3Tumor_Focalityspectrophotometer_method0.4609
4Tumor_Focalitywilms_tumor_histologic_subtype0.4346
5Tumor_Focalitybiospecimen_type0.4199
6Tumor_Focalitywgs_coverage0.4196
7Tumor_Focalityvascular_invasion_type0.4117
8Tumor_Focalityslides0.4094
9Tumor_Focalitytumor_depth_measurement0.3923
\n", + "
" + ], + "text/plain": [ + " source matches similarity\n", + "0 Tumor_Focality tumor_focality 0.6059\n", + "1 Tumor_Focality tumor_depth_descriptor 0.5419\n", + "2 Tumor_Focality tumor_shape 0.4857\n", + "3 Tumor_Focality spectrophotometer_method 0.4609\n", + "4 Tumor_Focality wilms_tumor_histologic_subtype 0.4346\n", + "5 Tumor_Focality biospecimen_type 0.4199\n", + "6 Tumor_Focality wgs_coverage 0.4196\n", + "7 Tumor_Focality vascular_invasion_type 0.4117\n", + "8 Tumor_Focality slides 0.4094\n", + "9 Tumor_Focality tumor_depth_measurement 0.3923" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bdi.top_matches(dataset, columns=['Tumor_Focality'], target='gdc')" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Some weights of RobertaModel were not initialized from the model checkpoint at roberta-base and are newly initialized: ['roberta.pooler.dense.bias', 'roberta.pooler.dense.weight']\n", + "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n", + "100%|██████████| 1/1 [00:00<00:00, 18.49it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Table features extracted from 1 columns\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 734/734 [00:49<00:00, 14.83it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Table features extracted from 734 columns\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcematchessimilarity
0Histologic_Grade_FIGOtumor_grade0.6154
1Histologic_Grade_FIGOwho_nte_grade0.581
2Histologic_Grade_FIGOinpc_grade0.5651
3Histologic_Grade_FIGOwho_cns_grade0.5456
4Histologic_Grade_FIGOhistologic_progression_type0.5343
5Histologic_Grade_FIGOigcccg_stage0.5319
6Histologic_Grade_FIGOtumor_grade_category0.5209
7Histologic_Grade_FIGOenneking_msts_grade0.4896
8Histologic_Grade_FIGOeducation_level0.4864
9Histologic_Grade_FIGOextrathyroid_extension0.4736
\n", + "
" + ], + "text/plain": [ + " source matches similarity\n", + "0 Histologic_Grade_FIGO tumor_grade 0.6154\n", + "1 Histologic_Grade_FIGO who_nte_grade 0.581\n", + "2 Histologic_Grade_FIGO inpc_grade 0.5651\n", + "3 Histologic_Grade_FIGO who_cns_grade 0.5456\n", + "4 Histologic_Grade_FIGO histologic_progression_type 0.5343\n", + "5 Histologic_Grade_FIGO igcccg_stage 0.5319\n", + "6 Histologic_Grade_FIGO tumor_grade_category 0.5209\n", + "7 Histologic_Grade_FIGO enneking_msts_grade 0.4896\n", + "8 Histologic_Grade_FIGO education_level 0.4864\n", + "9 Histologic_Grade_FIGO extrathyroid_extension 0.4736" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bdi.top_matches(dataset, columns=['Histologic_Grade_FIGO'], target='gdc')" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Some weights of RobertaModel were not initialized from the model checkpoint at roberta-base and are newly initialized: ['roberta.pooler.dense.bias', 'roberta.pooler.dense.weight']\n", + "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n", + "100%|██████████| 1/1 [00:00<00:00, 13.74it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Table features extracted from 1 columns\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 734/734 [00:54<00:00, 13.56it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Table features extracted from 734 columns\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcematchessimilarity
0Path_Stage_Primary_Tumor-pTuicc_clinical_stage0.7404
1Path_Stage_Primary_Tumor-pTajcc_clinical_stage0.6784
2Path_Stage_Primary_Tumor-pTuicc_pathologic_stage0.6754
3Path_Stage_Primary_Tumor-pTfigo_stage0.673
4Path_Stage_Primary_Tumor-pTajcc_pathologic_stage0.6702
5Path_Stage_Primary_Tumor-pTinss_stage0.6422
6Path_Stage_Primary_Tumor-pTensat_pathologic_stage0.598
7Path_Stage_Primary_Tumor-pTmasaoka_stage0.5898
8Path_Stage_Primary_Tumor-pTiss_stage0.5569
9Path_Stage_Primary_Tumor-pTann_arbor_clinical_stage0.5407
\n", + "
" + ], + "text/plain": [ + " source matches similarity\n", + "0 Path_Stage_Primary_Tumor-pT uicc_clinical_stage 0.7404\n", + "1 Path_Stage_Primary_Tumor-pT ajcc_clinical_stage 0.6784\n", + "2 Path_Stage_Primary_Tumor-pT uicc_pathologic_stage 0.6754\n", + "3 Path_Stage_Primary_Tumor-pT figo_stage 0.673\n", + "4 Path_Stage_Primary_Tumor-pT ajcc_pathologic_stage 0.6702\n", + "5 Path_Stage_Primary_Tumor-pT inss_stage 0.6422\n", + "6 Path_Stage_Primary_Tumor-pT ensat_pathologic_stage 0.598\n", + "7 Path_Stage_Primary_Tumor-pT masaoka_stage 0.5898\n", + "8 Path_Stage_Primary_Tumor-pT iss_stage 0.5569\n", + "9 Path_Stage_Primary_Tumor-pT ann_arbor_clinical_stage 0.5407" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bdi.top_matches(dataset, columns=['Path_Stage_Primary_Tumor-pT'], target='gdc')" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'tumor_grade': ['G1',\n", + " 'G2',\n", + " 'G3',\n", + " 'G4',\n", + " 'GB',\n", + " 'GX',\n", + " 'High Grade',\n", + " 'Intermediate Grade',\n", + " 'Low Grade',\n", + " 'Unknown',\n", + " 'Not Reported'],\n", + " 'who_nte_grade': ['G1', 'G2', 'G3', 'GX', 'Unknown', 'Not Reported']}\n" + ] + } + ], + "source": [ + "pprint(\n", + " get_gdc_data(\n", + " [\n", + " \"tumor_grade\",\n", + " \"who_nte_grade\",\n", + " ]\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Some weights of RobertaModel were not initialized from the model checkpoint at roberta-base and are newly initialized: ['roberta.pooler.dense.bias', 'roberta.pooler.dense.weight']\n", + "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n", + "100%|██████████| 1/1 [00:00<00:00, 18.26it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Table features extracted from 1 columns\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 734/734 [00:53<00:00, 13.68it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Table features extracted from 734 columns\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcematchessimilarity
0Ethnicityethnicity0.8297
1Ethnicityrace0.6577
2Ethnicityvariant_origin0.5169
3Ethnicitydemographics0.4102
4Ethnicitycountry_of_residence_at_enrollment0.3763
5Ethnicitycountry_of_birth0.3586
6Ethnicityeye_color0.3333
7Ethnicitymeasurement_type0.3144
8Ethnicityexposure_source0.314
9Ethnicityalcohol_type0.3134
\n", + "
" + ], + "text/plain": [ + " source matches similarity\n", + "0 Ethnicity ethnicity 0.8297\n", + "1 Ethnicity race 0.6577\n", + "2 Ethnicity variant_origin 0.5169\n", + "3 Ethnicity demographics 0.4102\n", + "4 Ethnicity country_of_residence_at_enrollment 0.3763\n", + "5 Ethnicity country_of_birth 0.3586\n", + "6 Ethnicity eye_color 0.3333\n", + "7 Ethnicity measurement_type 0.3144\n", + "8 Ethnicity exposure_source 0.314\n", + "9 Ethnicity alcohol_type 0.3134" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bdi.top_matches(dataset, columns=['Ethnicity'], target='gdc')" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcetargetsimilarity
0Hispanic or Latinohispanic or latino1.0
1Not-Hispanic or Latinonot hispanic or latino0.936364
2nan
3Not reported
\n", + "
" + ], + "text/plain": [ + " source target similarity\n", + "0 Hispanic or Latino hispanic or latino 1.0\n", + "1 Not-Hispanic or Latino not hispanic or latino 0.936364\n", + "2 nan \n", + "3 Not reported " + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bdi.preview_value_mappings(\n", + " dataset,\n", + " column_mapping=(\"Ethnicity\", \"ethnicity\"),\n", + " target=\"gdc\",\n", + " method=\"edit_distance\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcetargetsimilarity
0Whitewhite1.0
1Asianasian1.0
2Not Reportednot reported1.0
3Black or African Americanblack or african american1.0
4nan
\n", + "
" + ], + "text/plain": [ + " source target similarity\n", + "0 White white 1.0\n", + "1 Asian asian 1.0\n", + "2 Not Reported not reported 1.0\n", + "3 Black or African American black or african american 1.0\n", + "4 nan " + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bdi.preview_value_mappings(\n", + " dataset,\n", + " column_mapping=(\"Race\", \"race\"),\n", + " target=\"gdc\",\n", + " method=\"edit_distance\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcetargetsimilarity
0UnifocalUnifocal1.0
1MultifocalMultifocal1.0
2nan
\n", + "
" + ], + "text/plain": [ + " source target similarity\n", + "0 Unifocal Unifocal 1.0\n", + "1 Multifocal Multifocal 1.0\n", + "2 nan " + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bdi.preview_value_mappings(\n", + " dataset,\n", + " column_mapping=(\"Tumor_Focality\", \"tumor_focality\"),\n", + " target=\"gdc\",\n", + " method=\"edit_distance\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "column_mappings = pd.DataFrame(\n", + " [\n", + " {\n", + " \"source\": \"Ethnicity\",\n", + " \"target\": \"ethnicity\",\n", + " },\n", + " {\n", + " \"source\": \"Gender\",\n", + " \"target\": \"gender\",\n", + " },\n", + " {\n", + " \"source\": \"Race\",\n", + " \"target\": \"race\",\n", + " },\n", + " {\n", + " \"source\": \"Country\",\n", + " \"target\": \"country_of_birth\",\n", + " },\n", + " {\n", + " \"source\": \"Tumor_Focality\",\n", + " \"target\": \"tumor_focality\",\n", + " },\n", + " {\n", + " \"source\": \"FIGO_stage\",\n", + " \"target\": \"figo_stage\",\n", + " },\n", + " {\n", + " \"source\": \"Histologic_Grade_FIGO\",\n", + " \"target\": \"tumor_grade\",\n", + " },\n", + " {\n", + " \"source\": \"BMI\",\n", + " \"target\": \"bmi\",\n", + " }\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ethnicity => ethnicity\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcetargetsimilarity
0Hispanic or Latinohispanic or latino1.0
1Not-Hispanic or Latinonot hispanic or latino0.936364
2nan
3Not reported
\n", + "
" + ], + "text/plain": [ + " source target similarity\n", + "0 Hispanic or Latino hispanic or latino 1.0\n", + "1 Not-Hispanic or Latino not hispanic or latino 0.936364\n", + "2 nan \n", + "3 Not reported " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Gender => gender\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcetargetsimilarity
0Femalefemale1.0
1nan
\n", + "
" + ], + "text/plain": [ + " source target similarity\n", + "0 Female female 1.0\n", + "1 nan " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Race => race\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcetargetsimilarity
0Whitewhite1.0
1Asianasian1.0
2Not Reportednot reported1.0
3Black or African Americanblack or african american1.0
4nan
\n", + "
" + ], + "text/plain": [ + " source target similarity\n", + "0 White white 1.0\n", + "1 Asian asian 1.0\n", + "2 Not Reported not reported 1.0\n", + "3 Black or African American black or african american 1.0\n", + "4 nan " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Country => country_of_birth\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcetargetsimilarity
0United StatesUnited States1.0
1UkraineUkraine1.0
2PolandPoland1.0
3nan
4Other_specify
\n", + "
" + ], + "text/plain": [ + " source target similarity\n", + "0 United States United States 1.0\n", + "1 Ukraine Ukraine 1.0\n", + "2 Poland Poland 1.0\n", + "3 nan \n", + "4 Other_specify " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Tumor_Focality => tumor_focality\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcetargetsimilarity
0UnifocalUnifocal1.0
1MultifocalMultifocal1.0
2nan
\n", + "
" + ], + "text/plain": [ + " source target similarity\n", + "0 Unifocal Unifocal 1.0\n", + "1 Multifocal Multifocal 1.0\n", + "2 nan " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "FIGO_stage => figo_stage\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcetargetsimilarity
0IIIC2Stage IIIC21.0
1IIIC1Stage IIIC11.0
2IB
3nan
4IVB
5IIIB
6IA
7II
8IIIA
\n", + "
" + ], + "text/plain": [ + " source target similarity\n", + "0 IIIC2 Stage IIIC2 1.0\n", + "1 IIIC1 Stage IIIC1 1.0\n", + "2 IB \n", + "3 nan \n", + "4 IVB \n", + "5 IIIB \n", + "6 IA \n", + "7 II \n", + "8 IIIA " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Histologic_Grade_FIGO => tumor_grade\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcetargetsimilarity
0FIGO grade 1High Grade1.0
1FIGO grade 2High Grade1.0
2FIGO grade 3High Grade1.0
3nan
\n", + "
" + ], + "text/plain": [ + " source target similarity\n", + "0 FIGO grade 1 High Grade 1.0\n", + "1 FIGO grade 2 High Grade 1.0\n", + "2 FIGO grade 3 High Grade 1.0\n", + "3 nan " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "mappings = bdi.preview_value_mappings(\n", + " dataset,\n", + " column_mapping=column_mappings,\n", + " target=\"gdc\",\n", + " method=\"edit_distance\",\n", + ")\n", + "\n", + "for mapping in mappings:\n", + " print(f\"{mapping['source']} => {mapping['target']}\")\n", + " display(mapping[\"mapping\"])\n", + " print(\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "value_mappings = bdi.match_values(\n", + " dataset, target=\"gdc\", column_mapping=column_mappings, method=\"edit_distance\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "from bdikit import update_mappings\n", + "import re\n", + "\n", + "\n", + "def map_hist_grade_figo(value_str):\n", + " if isinstance(value_str, str):\n", + " return re.sub(r\"FIGO grade (\\d+)\", r\"G\\1\", value_str)\n", + " else:\n", + " return \"Unknown\"\n", + "\n", + "\n", + "user_mappings = [\n", + " {\n", + " \"source\": \"BMI\",\n", + " \"target\": \"bmi\",\n", + " },\n", + " {\n", + " \"source\": \"Age\",\n", + " \"target\": \"days_to_birth\",\n", + " \"mapper\": lambda age: -age * 365,\n", + " },\n", + " {\n", + " \"source\": \"Histologic_Grade_FIGO\",\n", + " \"target\": \"tumor_grade\",\n", + " \"mapper\": map_hist_grade_figo,\n", + " },\n", + " {\n", + " \"source\": \"FIGO_stage\",\n", + " \"target\": \"figo_stage\",\n", + " \"mapper\": lambda x: f\"Stage {x}\" if isinstance(x, str) else \"Unknown\",\n", + " },\n", + "]\n", + "\n", + "harmonization_spec = update_mappings(value_mappings, user_mappings)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ethnicitygenderracecountry_of_birthtumor_focalitybmidays_to_birthtumor_gradefigo_stage
0not hispanic or latinofemalewhiteUnited StatesUnifocal38.88-23360.0G1Stage IA
1not hispanic or latinofemalewhiteUnited StatesUnifocal39.76-21170.0G1Stage IA
2not hispanic or latinofemalewhiteUnited StatesUnifocal51.19-18250.0G2Stage IA
3NaNNaNNaNNaNNaNNaNNaNUnknownUnknown
4not hispanic or latinofemalewhiteUnited StatesUnifocal32.69-27375.0G2Stage IA
..............................
99NaNfemaleNaNUkraineUnifocal29.40-27375.0G3Stage IA
100NaNfemaleNaNUkraineUnifocal35.42-27010.0G2Stage II
101not hispanic or latinofemaleblack or african americanUnited StatesUnifocal24.32-31025.0UnknownStage II
102NaNfemaleNaNUkraineUnifocal34.06-25550.0UnknownStage IA
103NaNNaNNaNUkraineNaNNaNNaNUnknownUnknown
\n", + "

104 rows × 9 columns

\n", + "
" + ], + "text/plain": [ + " ethnicity gender race \\\n", + "0 not hispanic or latino female white \n", + "1 not hispanic or latino female white \n", + "2 not hispanic or latino female white \n", + "3 NaN NaN NaN \n", + "4 not hispanic or latino female white \n", + ".. ... ... ... \n", + "99 NaN female NaN \n", + "100 NaN female NaN \n", + "101 not hispanic or latino female black or african american \n", + "102 NaN female NaN \n", + "103 NaN NaN NaN \n", + "\n", + " country_of_birth tumor_focality bmi days_to_birth tumor_grade \\\n", + "0 United States Unifocal 38.88 -23360.0 G1 \n", + "1 United States Unifocal 39.76 -21170.0 G1 \n", + "2 United States Unifocal 51.19 -18250.0 G2 \n", + "3 NaN NaN NaN NaN Unknown \n", + "4 United States Unifocal 32.69 -27375.0 G2 \n", + ".. ... ... ... ... ... \n", + "99 Ukraine Unifocal 29.40 -27375.0 G3 \n", + "100 Ukraine Unifocal 35.42 -27010.0 G2 \n", + "101 United States Unifocal 24.32 -31025.0 Unknown \n", + "102 Ukraine Unifocal 34.06 -25550.0 Unknown \n", + "103 Ukraine NaN NaN NaN Unknown \n", + "\n", + " figo_stage \n", + "0 Stage IA \n", + "1 Stage IA \n", + "2 Stage IA \n", + "3 Unknown \n", + "4 Stage IA \n", + ".. ... \n", + "99 Stage IA \n", + "100 Stage II \n", + "101 Stage II \n", + "102 Stage IA \n", + "103 Unknown \n", + "\n", + "[104 rows x 9 columns]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "harmonized_dataset = bdi.materialize_mapping(dataset, harmonization_spec)\n", + "harmonized_dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
EthnicityGenderRaceCountryTumor_FocalityBMIAgeHistologic_Grade_FIGOFIGO_stage
0Not-Hispanic or LatinoFemaleWhiteUnited StatesUnifocal38.8864.0FIGO grade 1IA
1Not-Hispanic or LatinoFemaleWhiteUnited StatesUnifocal39.7658.0FIGO grade 1IA
2Not-Hispanic or LatinoFemaleWhiteUnited StatesUnifocal51.1950.0FIGO grade 2IA
3NaNNaNNaNNaNNaNNaNNaNNaNNaN
4Not-Hispanic or LatinoFemaleWhiteUnited StatesUnifocal32.6975.0FIGO grade 2IA
..............................
99NaNFemaleNaNUkraineUnifocal29.4075.0FIGO grade 3IA
100NaNFemaleNaNUkraineUnifocal35.4274.0FIGO grade 2II
101Not-Hispanic or LatinoFemaleBlack or African AmericanUnited StatesUnifocal24.3285.0NaNII
102NaNFemaleNaNUkraineUnifocal34.0670.0NaNIA
103NaNNaNNaNUkraineNaNNaNNaNNaNNaN
\n", + "

104 rows × 9 columns

\n", + "
" + ], + "text/plain": [ + " Ethnicity Gender Race Country \\\n", + "0 Not-Hispanic or Latino Female White United States \n", + "1 Not-Hispanic or Latino Female White United States \n", + "2 Not-Hispanic or Latino Female White United States \n", + "3 NaN NaN NaN NaN \n", + "4 Not-Hispanic or Latino Female White United States \n", + ".. ... ... ... ... \n", + "99 NaN Female NaN Ukraine \n", + "100 NaN Female NaN Ukraine \n", + "101 Not-Hispanic or Latino Female Black or African American United States \n", + "102 NaN Female NaN Ukraine \n", + "103 NaN NaN NaN Ukraine \n", + "\n", + " Tumor_Focality BMI Age Histologic_Grade_FIGO FIGO_stage \n", + "0 Unifocal 38.88 64.0 FIGO grade 1 IA \n", + "1 Unifocal 39.76 58.0 FIGO grade 1 IA \n", + "2 Unifocal 51.19 50.0 FIGO grade 2 IA \n", + "3 NaN NaN NaN NaN NaN \n", + "4 Unifocal 32.69 75.0 FIGO grade 2 IA \n", + ".. ... ... ... ... ... \n", + "99 Unifocal 29.40 75.0 FIGO grade 3 IA \n", + "100 Unifocal 35.42 74.0 FIGO grade 2 II \n", + "101 Unifocal 24.32 85.0 NaN II \n", + "102 Unifocal 34.06 70.0 NaN IA \n", + "103 NaN NaN NaN NaN NaN \n", + "\n", + "[104 rows x 9 columns]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "original_columns = map(lambda m: m[\"source\"], harmonization_spec)\n", + "dataset[original_columns]" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/test_api.py b/tests/test_api.py index 39248a1c..b9a40c6c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -146,7 +146,7 @@ def test_map_column_values(): ) # then - upper_cased_values = ["A", "B", "C", "D", "E"] + upper_cased_values = pd.Series(["A", "B", "C", "D", "E"]) assert mapped_column.name == target_column_name assert mapped_column.eq(upper_cased_values).all() @@ -159,19 +159,19 @@ def test_map_dataframe_column_values(): value_mapping_spec = [ { - "from": "column_str_1", - "to": "string column 1", + "source": "column_str_1", + "target": "string column 1", "mapper": IdentityValueMapper(), }, { - "from": "column_str_2", - "to": "string column 2", + "source": "column_str_2", + "target": "string column 2", "mapper": FunctionValueMapper(function=lambda x: x.upper()), }, ] # when - df_mapped = bdi.materialize_mapping(df_base, target=value_mapping_spec) + df_mapped = bdi.materialize_mapping(df_base, mapping_spec=value_mapping_spec) # then assert len(df_mapped.columns) == 2 @@ -181,3 +181,33 @@ def test_map_dataframe_column_values(): assert "string column 2" in df_mapped.columns assert df_mapped["string column 2"].eq(["A", "B", "C", "D", "E"]).all() + + +def test_value_mapping_dataframe(): + # given + df_source = pd.DataFrame( + {"src_column": ["Red Apple", "Banana", "Oorange", "Strawberry"]} + ) + df_target = pd.DataFrame( + {"tgt_column": ["apple", "banana", "orange", "kiwi", "grapes"]} + ) + + df_matches = pd.DataFrame({"source": ["src_column"], "target": ["tgt_column"]}) + + # when + value_mappings = bdi.match_values(df_source, df_target, df_matches, method="tfidf") + + # then + assert value_mappings is not None + assert "src_column" in value_mappings + assert value_mappings["src_column"]["matches"] is not None + assert value_mappings["src_column"]["target"] == "tgt_column" + + src_column_mapping = value_mappings["src_column"] + assert len(src_column_mapping["matches"]) == 3 + assert len(src_column_mapping["matches"]) == 3 + + +# TODO +# def test_preview_value_mappings(): +# pass From 1e71758799747fee8bdb532a1a9ece5659a0fd99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=C3=A9cio=20Santos?= Date: Thu, 20 Jun 2024 19:37:50 -0400 Subject: [PATCH 10/22] test(api): Add end-to-end API integration test --- .../value_mapping/value_mappers.py | 9 +++-- tests/test_api.py | 40 +++++++++++++++++-- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/bdikit/mapping_algorithms/value_mapping/value_mappers.py b/bdikit/mapping_algorithms/value_mapping/value_mappers.py index 5b8b635d..d6ad11d3 100644 --- a/bdikit/mapping_algorithms/value_mapping/value_mappers.py +++ b/bdikit/mapping_algorithms/value_mapping/value_mappers.py @@ -1,5 +1,6 @@ import pandas as pd -from typing import Callable +from typing import Any, Callable +from collections import defaultdict class ValueMapper: @@ -52,12 +53,12 @@ class DictionaryMapper(ValueMapper): values stored in the provided dictionary. """ - def __init__(self, dictionary: dict): - self.dictionary = dictionary + def __init__(self, dictionary: dict, missing_data_value: Any = None): + self.dictionary = defaultdict(lambda: missing_data_value, dictionary) def map(self, input_column: pd.Series) -> pd.Series: """ Transforms the values in the input_column to the values specified in the dictionary provided using the object constructor. """ - return input_column.map(self.dictionary) + return input_column.map(self.dictionary, na_action="ignore") diff --git a/tests/test_api.py b/tests/test_api.py index b9a40c6c..1d21ae74 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -208,6 +208,40 @@ def test_value_mapping_dataframe(): assert len(src_column_mapping["matches"]) == 3 -# TODO -# def test_preview_value_mappings(): -# pass +def test_end_to_end_api_integration(): + # given + df_source = pd.DataFrame( + {"src_column": ["Red Apple", "Banana", "Oorange", "Strawberry"]} + ) + df_target = pd.DataFrame( + {"tgt_column": ["apple", "banana", "orange", "kiwi", "grapes"]} + ) + + # when + column_mappings = bdi.match_columns(df_source, df_target, method="coma") + # then + assert column_mappings is not None + assert column_mappings.empty == False + assert "source" in column_mappings.columns + assert "target" in column_mappings.columns + + # when + value_mappings = bdi.match_values( + df_source, df_target, column_mappings, method="tfidf" + ) + + assert value_mappings is not None + assert "src_column" in value_mappings + assert value_mappings["src_column"]["matches"] is not None + assert value_mappings["src_column"]["target"] == "tgt_column" + + src_column_mapping = value_mappings["src_column"] + assert len(src_column_mapping["matches"]) == 3 + assert len(src_column_mapping["matches"]) == 3 + + # when + harmonization_spec = bdi.update_mappings(value_mappings, []) + df_mapped = bdi.materialize_mapping(df_source, harmonization_spec) + + assert "tgt_column" in df_mapped.columns + assert df_mapped["tgt_column"].tolist() == ["apple", "banana", "orange", None] From 3fc6b9481c9ddaa857a78e3069dbf41e8dc1e158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=C3=A9cio=20Santos?= Date: Fri, 21 Jun 2024 14:50:34 -0400 Subject: [PATCH 11/22] feat(api): Add bdi.preview_domains() --- bdikit/functional_api.py | 59 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/bdikit/functional_api.py b/bdikit/functional_api.py index e4f2bbf2..b860c77f 100644 --- a/bdikit/functional_api.py +++ b/bdikit/functional_api.py @@ -366,6 +366,65 @@ def preview_value_mappings( return result +def preview_domains( + dataset: pd.DataFrame, + column_mapping: Tuple[str, str], + target: Union[str, pd.DataFrame] = "gdc", + limit: int = 10, +) -> pd.DataFrame: + """ + Preview the domain of the given columns in the source and target datasets. + + Args: + dataset (pd.DataFrame): The source dataset containing the columns to preview. + column_mapping (Tuple[str, str]): The mapping between the source and target columns. + The first and second positions should contain the names of the + source and target columns respectively. + target (Union[str, pd.DataFrame], optional): The target dataset or standard vocabulary name. + If a string is provided and it is equal to "gdc", the target domain will be retrieved from the GDC data. + If a DataFrame is provided, the target domain will be retrieved from the specified DataFrame. + Defaults to "gdc". + limit (int, optional): The maximum number of unique values to include in the preview. + Defaults to 10. + + Returns: + pd.DataFrame: A DataFrame containing a sample of the source and target domain values. + The DataFrame will have two columns: "source_domain_sample" and "target_domain_sample". + """ + source_column, target_column = column_mapping + + source_domain = dataset[source_column].unique() + + if isinstance(target, str) and target == "gdc": + target_domain = np.array(get_gdc_data([target_column])[target_column]) + elif isinstance(target, pd.DataFrame): + target_domain = target[target_column].unique() + else: + raise ValueError( + "The target must be a DataFrame or a standard vocabulary name." + ) + + output_size = min(max(len(source_domain), len(target_domain)), limit) + + if len(source_domain) > output_size: + source_domain = source_domain[:output_size] + if len(target_domain) > output_size: + target_domain = target_domain[:output_size] + + if len(source_domain) < output_size: + source_domain = np.append( + source_domain, np.full(output_size - len(source_domain), "") + ) + if len(target_domain) < output_size: + target_domain = np.append( + target_domain, np.full(output_size - len(target_domain), "") + ) + + return pd.DataFrame( + {"source_domain_sample": source_domain, "target_domain_sample": target_domain} + ) + + def update_mappings(value_mappings: Dict, user_mappings: List) -> List: user_mappings_dict = { user_mapping["source"] + "__" + user_mapping["target"]: user_mapping From ebe528b4085f5c3717089e51c822d3a53b82a2f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=C3=A9cio=20Santos?= Date: Fri, 21 Jun 2024 21:17:40 -0400 Subject: [PATCH 12/22] refactor(api): Make API inputs more compatible --- bdikit/functional_api.py | 226 ++++++++++++++++++++++++++++----------- tests/test_api.py | 76 ++++++++++--- 2 files changed, 223 insertions(+), 79 deletions(-) diff --git a/bdikit/functional_api.py b/bdikit/functional_api.py index b860c77f..f425c398 100644 --- a/bdikit/functional_api.py +++ b/bdikit/functional_api.py @@ -1,6 +1,7 @@ from enum import Enum from os.path import join, dirname from typing import Union, Type, List, Dict, TypedDict, Set, Optional, Tuple, Callable +import itertools import pandas as pd import numpy as np from bdikit.utils import get_gdc_data @@ -167,7 +168,7 @@ def get_instance(method_name: str) -> BaseAlgorithm: def materialize_mapping( - input_table: pd.DataFrame, mapping_spec: List[dict] + input_table: pd.DataFrame, mapping_spec: Union[List[dict], pd.DataFrame] ) -> pd.DataFrame: """ Takes an input DataFrame and a target mapping specification and returns a @@ -178,6 +179,17 @@ def materialize_mapping( mapper that is used to transform the values of the input column to the output column. """ + if isinstance(mapping_spec, pd.DataFrame): + mapping_spec = mapping_spec.to_dict(orient="records") + + for mapping in mapping_spec: + if "source" not in mapping or "target" not in mapping: + raise ValueError( + "Each mapping specification should contain 'source', 'target' and 'mapper' (optional) keys." + ) + if "mapper" not in mapping: + mapping["mapper"] = create_mapper(mapping) + output_dataframe = pd.DataFrame() for column_spec in mapping_spec: from_column_name = column_spec["source"] @@ -198,6 +210,7 @@ def map_column_values( class ValueMatchingResult(TypedDict): + source: str target: str matches: List[ValueMatch] coverage: float @@ -210,7 +223,7 @@ def match_values( target: Union[str, pd.DataFrame], column_mapping: pd.DataFrame, method: str = ValueMatchingMethod.EDIT.name, -) -> Dict[str, ValueMatchingResult]: +) -> List[ValueMatchingResult]: """ Maps the values of the dataset columns to the target domain using the given method name. """ @@ -238,9 +251,9 @@ def _match_values( target_domain: Dict[str, Optional[List[str]]], column_mapping: Dict[str, str], value_matcher: BaseAlgorithm, -) -> Dict[str, ValueMatchingResult]: +) -> List[ValueMatchingResult]: - mapping_results: dict[str, ValueMatchingResult] = {} + mapping_results: List[ValueMatchingResult] = [] for source_column, target_column in column_mapping.items(): @@ -280,12 +293,15 @@ def _match_values( source_values = set(source_values_dict.values()) match_values = set([x[0] for x in matches]) - mapping_results[source_column] = ValueMatchingResult( - target=target_column, - matches=matches, - coverage=coverage, - unique_values=source_values, - unmatch_values=source_values - match_values, + mapping_results.append( + ValueMatchingResult( + source=source_column, + target=target_column, + matches=matches, + coverage=coverage, + unique_values=source_values, + unmatch_values=source_values - match_values, + ) ) return mapping_results @@ -330,7 +346,8 @@ def preview_value_mappings( ) result = [] - for source_column, matching_result in value_mappings.items(): + for matching_result in value_mappings: + # transform matches and unmatched values into DataFrames matches_df = pd.DataFrame( data=matching_result["matches"], @@ -351,7 +368,7 @@ def preview_value_mappings( result.append( { - "source": source_column, + "source": matching_result["source"], "target": matching_result["target"], "mapping": pd.concat([matches_df, unmatched_df], ignore_index=True), } @@ -425,82 +442,167 @@ def preview_domains( ) -def update_mappings(value_mappings: Dict, user_mappings: List) -> List: - user_mappings_dict = { - user_mapping["source"] + "__" + user_mapping["target"]: user_mapping - for user_mapping in user_mappings - } +ValueMatchingLike = Union[List[ValueMatchingResult], List[Dict], pd.DataFrame] + + +def update_mappings( + value_mappings: ValueMatchingLike, user_mappings: Optional[ValueMatchingLike] = None +) -> List: + + if user_mappings is None: + user_mappings = [] + + if isinstance(value_mappings, pd.DataFrame): + value_mappings = value_mappings.to_dict(orient="records") + + if isinstance(user_mappings, pd.DataFrame): + user_mappings = user_mappings.to_dict(orient="records") + def create_key(source: str, target: str) -> str: + return source + "__" + target + + def check_duplicates(mappings: List): + keys = set() + for mapping in mappings: + key = create_key(mapping["source"], mapping["target"]) + if key in keys: + raise ValueError( + f"Duplicate mapping for source: {mapping['source']}, target: {mapping['target']}" + ) + keys.add(key) + + # first check duplicates in each individual list + check_duplicates(user_mappings) + check_duplicates(value_mappings) + + mapping_keys = set() final_mappings = [] - for source_column, mapping in value_mappings.items(): - # if the mapping is provided by the user, we ignore it here - # since the user mappings take precedence - key = source_column + "__" + mapping["target"] - if key not in user_mappings_dict: - final_mappings.append( - { - "source": source_column, - "target": mapping["target"], - "mapper": create_mapper(mapping), - } - ) - # include all user mappings - for user_mapping in user_mappings: - mapper_spec = user_mapping.get("mapper", None) - if not isinstance(mapper_spec, ValueMapper): - user_mapping["mapper"] = create_mapper(mapper_spec) - final_mappings.append(user_mapping) + # include all unique user mappings first, as they take precedence + for mapping in itertools.chain(user_mappings, value_mappings): + + source_column = mapping["source"] + target_column = mapping["target"] + + # ignore duplicate mappings accross user and value mappings + key = create_key(source_column, target_column) + if key in mapping_keys: + continue + else: + mapping_keys.add(key) + + # try creating a mapper object from the mapping + mapper = create_mapper(mapping) + + final_mappings.append( + { + "source": source_column, + "target": target_column, + "mapper": mapper, + } + ) return final_mappings def create_mapper( - input: Union[None, pd.DataFrame, Dict, Callable[[pd.Series], pd.Series]] + input: Union[ + None, + ValueMapper, + pd.DataFrame, + ValueMatchingResult, + List[ValueMatch], + Dict, + Callable[[pd.Series], pd.Series], + ] ): """ Tries to instantiate an appropriate ValueMapper object for the given input argument. - Depending on the input type, it creates one of the following objects: - - If input is a function (or lambda function), it creates a FunctionValueMapper object. - - If input is a dictionary or Pandas DataFrame, it creates a DictionaryMapper object. + Depending on the input type, it may create one of the following objects: - If input is None, it creates an IdentityValueMapper object. + - If input is a ValueMapper, it returns the input object. + - If input is a function (or lambda function), it creates a FunctionValueMapper object. + - If input is a list of ValueMatch objects or tuples (, ), + it creates a DictionaryMapper object. + - If input is a DataFrame with two columns ("current_value", "target_value"), + it creates a DictionaryMapper object. + - If input is a dictionary containing a "source" and "target" key, it tries to create + a ValueMapper object based on the specification given in "mapper" or "matches" keys. + + Args: + input: + The input argument to create a ValueMapper object from. + + Returns: + ValueMapper: An instance of a ValueMapper. """ + # If no input is provided, we create an IdentityValueMapper by default + # to not change the values from the source column if input is None: return IdentityValueMapper() + # If the input is already a ValueMapper, no need to create a new one if isinstance(input, ValueMapper): return input + # If the input is a function, we can create a FunctionValueMapper + # that applies the function to the values of the source column if callable(input): return FunctionValueMapper(input) - if ( - isinstance(input, dict) - and "matches" in input - and isinstance(input["matches"], list) - ): - # This is a dictionary returned by match_values function - matches = input["matches"] - mapping_dict = {} - for match in matches: - if isinstance(match, ValueMatch): - mapping_dict[match.current_value] = match.target_value - elif isinstance(match, tuple) and len(match) >= 2: - if isinstance(match[0], str) and isinstance(match[1], str): - mapping_dict[match[0]] = match[1] - else: - raise ValueError( - "Tuple in matches must contain two strings: (current_value, target_value)" - ) - else: - raise ValueError( - "Matches must be a list of ValueMatch objects or tuples" - ) - return DictionaryMapper(mapping_dict) + # This could be a list of value matches produced by match_values(), + # so can create a DictionaryMapper based on the value matches + if isinstance(input, List): + return _create_mapper_from_value_matches(input) + # If the input is a DataFrame with two columns, we can create a + # DictionaryMapper based on the values in the DataFrame if isinstance(input, pd.DataFrame) and all( k in input.columns for k in ["current_value", "target_value"] ): return DictionaryMapper( input.set_index("current_value")["target_value"].to_dict() ) + + if isinstance(input, Dict): + if all(k in input for k in ["source", "target"]): + # This could be the mapper created by update_mappings() or a + # specification defined by the user + if "mapper" in input: + if isinstance(input["mapper"], ValueMapper): + # If it contains a ValueMapper object, just return it + return input["mapper"] + else: + # Else, 'mapper' may contain one of the basic values that + # can be used to create a ValueMapper object defined above, + # so call this funtion recursively create it + return create_mapper(input["mapper"]) + + # This could be the ouput of match_values(), so can create a + # DictionaryMapper based on the value matches + if "matches" in input and isinstance(input["matches"], List): + return _create_mapper_from_value_matches(input["matches"]) + + # This could be the output of match_columns(), but the user did not + # define any mapper, so we create an IdentityValueMapper to map the + # column to the target name but keeping the values as they are + return IdentityValueMapper() + + raise ValueError(f"Failed to create a ValueMapper for given input: {input}") + + +def _create_mapper_from_value_matches(matches: List[ValueMatch]) -> DictionaryMapper: + mapping_dict = {} + for match in matches: + if isinstance(match, ValueMatch): + mapping_dict[match.current_value] = match.target_value + elif isinstance(match, tuple) and len(match) == 2: + if isinstance(match[0], str) and isinstance(match[1], str): + mapping_dict[match[0]] = match[1] + else: + raise ValueError( + "Tuple in matches must contain two strings: (source_value, target_value)" + ) + else: + raise ValueError("Matches must be a list of ValueMatch objects or tuples") + return DictionaryMapper(mapping_dict) diff --git a/tests/test_api.py b/tests/test_api.py index 1d21ae74..53cb1a77 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -198,14 +198,13 @@ def test_value_mapping_dataframe(): value_mappings = bdi.match_values(df_source, df_target, df_matches, method="tfidf") # then - assert value_mappings is not None - assert "src_column" in value_mappings - assert value_mappings["src_column"]["matches"] is not None - assert value_mappings["src_column"]["target"] == "tgt_column" - - src_column_mapping = value_mappings["src_column"] - assert len(src_column_mapping["matches"]) == 3 - assert len(src_column_mapping["matches"]) == 3 + assert len(value_mappings) == 1 + mapping = value_mappings[0] + assert mapping is not None + assert mapping["source"] == "src_column" + assert mapping["target"] == "tgt_column" + assert isinstance(mapping["matches"], list) + assert len(mapping["matches"]) == 3 def test_end_to_end_api_integration(): @@ -224,24 +223,67 @@ def test_end_to_end_api_integration(): assert column_mappings.empty == False assert "source" in column_mappings.columns assert "target" in column_mappings.columns + assert len(column_mappings.index) == 1 - # when + # when: pass output of match_columns() directly to materialize_mapping(), + # the column must be ranamed to the target column without any value mapping + df_mapped = bdi.materialize_mapping(df_source, column_mappings) + # then + assert "tgt_column" in df_mapped.columns + assert df_mapped["tgt_column"].tolist() == [ + "Red Apple", + "Banana", + "Oorange", + "Strawberry", + ] + + # when: we pass the output of match_columns() value_mappings = bdi.match_values( df_source, df_target, column_mappings, method="tfidf" ) - assert value_mappings is not None - assert "src_column" in value_mappings - assert value_mappings["src_column"]["matches"] is not None - assert value_mappings["src_column"]["target"] == "tgt_column" + # then: a list of value matches must be computed + assert len(value_mappings) == 1 + mapping = value_mappings[0] + assert mapping is not None + assert mapping["source"] == "src_column" + assert mapping["target"] == "tgt_column" + assert isinstance(mapping["matches"], list) + assert len(mapping["matches"]) == 3 - src_column_mapping = value_mappings["src_column"] - assert len(src_column_mapping["matches"]) == 3 - assert len(src_column_mapping["matches"]) == 3 + # when: pass output of match_values() to materialize_mapping(), + df_mapped = bdi.materialize_mapping(df_source, value_mappings) - # when + # then: the column must be ranamed and values must be mapped to the + # matching values found during the value matching step + assert "tgt_column" in df_mapped.columns + assert df_mapped["tgt_column"].tolist() == ["apple", "banana", "orange", None] + + # when: pass output of match_values() to update_mappings() and then to + # materialize_mapping() harmonization_spec = bdi.update_mappings(value_mappings, []) df_mapped = bdi.materialize_mapping(df_source, harmonization_spec) + # then: the column must be ranamed and values must be mapped assert "tgt_column" in df_mapped.columns assert df_mapped["tgt_column"].tolist() == ["apple", "banana", "orange", None] + + # when: user mappings are specified in update_mappings() + user_mappings = [ + { + "source": "src_column", + "target": "tgt_column", + "matches": [ + ("Red Apple", "APPLE"), + ("Banana", "BANANA"), + ("Oorange", "ORANGE"), + ], + } + ] + harmonization_spec = bdi.update_mappings(value_mappings, user_mappings) + df_mapped = bdi.materialize_mapping(df_source, harmonization_spec) + + # then: user mappings take precedence, so the column must be ranamed and + # values must be mapped according the provide user_mappings + assert "tgt_column" in df_mapped.columns + assert df_mapped["tgt_column"].tolist() == ["APPLE", "BANANA", "ORANGE", None] From c1d381237a5e2eb8f9f8d7e4941010d3082aa906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=C3=A9cio=20Santos?= Date: Mon, 24 Jun 2024 15:53:41 -0400 Subject: [PATCH 13/22] feat(api): Support an object as a method in match_columns() --- bdikit/functional_api.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/bdikit/functional_api.py b/bdikit/functional_api.py index f425c398..fb852091 100644 --- a/bdikit/functional_api.py +++ b/bdikit/functional_api.py @@ -76,18 +76,39 @@ def get_instance(method_name: str) -> BaseColumnMappingAlgorithm: def match_columns( source: pd.DataFrame, target: Union[str, pd.DataFrame] = "gdc", - method: str = ColumnMappingMethod.SIMFLOOD.name, + method: Union[str, BaseColumnMappingAlgorithm] = ColumnMappingMethod.SIMFLOOD.name, ) -> pd.DataFrame: """ - Performs schema mapping between the source table and the given target. The target - either is a DataFrame or a string representing a standard data vocabulary. + Performs schema mapping between the source table and the given target schema. The + target either is a DataFrame or a string representing a standard data vocabulary + supported by the library. Currently, only the GDC (Genomic Data Commons) standard + vocabulary is supported. + + Parameters: + source (pd.DataFrame): The source table to be mapped. + target (Union[str, pd.DataFrame], optional): The target table or standard data vocabulary. Defaults to "gdc". + method (Union[str, BaseColumnMappingAlgorithm], optional): The method used for mapping. Defaults to ColumnMappingMethod.SIMFLOOD.name. + + Returns: + pd.DataFrame: A DataFrame containing the mapping results with columns "source" and "target". + + Raises: + ValueError: If the method is neither a string nor an instance of BaseColumnMappingAlgorithm. """ if isinstance(target, str): target_table = _load_table_for_standard(target) else: target_table = target - matcher_instance = ColumnMappingMethod.get_instance(method) + if isinstance(method, str): + matcher_instance = ColumnMappingMethod.get_instance(method) + elif isinstance(method, BaseColumnMappingAlgorithm): + matcher_instance = method + else: + raise ValueError( + "The method must be a string or an instance of BaseColumnMappingAlgorithm" + ) + matches = matcher_instance.map(source, target_table) return pd.DataFrame(matches.items(), columns=["source", "target"]) From c84023ad48b136859b711d76ed517590422f0aa0 Mon Sep 17 00:00:00 2001 From: Roque Lopez Date: Tue, 25 Jun 2024 11:56:06 -0400 Subject: [PATCH 14/22] Cast dictionary key to string --- bdikit/functional_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bdikit/functional_api.py b/bdikit/functional_api.py index fb852091..a98379a7 100644 --- a/bdikit/functional_api.py +++ b/bdikit/functional_api.py @@ -291,7 +291,9 @@ def _match_values( source_values_dict: Dict[str, str] = { str(x).strip().lower(): str(x).strip() for x in unique_values } - target_values_dict: Dict[str, str] = {x.lower(): x for x in target_domain_list} + target_values_dict: Dict[str, str] = { + str(x).lower(): x for x in target_domain_list + } # 3. Apply the value matcher to create value mapping dictionaries matches_lowercase = value_matcher.match( From 718b3d6be04d31145ec1829f805389e235412919 Mon Sep 17 00:00:00 2001 From: Roque Lopez Date: Tue, 25 Jun 2024 11:59:46 -0400 Subject: [PATCH 15/22] Fix #57 --- bdikit/mapping_algorithms/value_mapping/algorithms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bdikit/mapping_algorithms/value_mapping/algorithms.py b/bdikit/mapping_algorithms/value_mapping/algorithms.py index 08e5c438..6edbff39 100644 --- a/bdikit/mapping_algorithms/value_mapping/algorithms.py +++ b/bdikit/mapping_algorithms/value_mapping/algorithms.py @@ -68,7 +68,7 @@ class TFIDFAlgorithm(PolyFuzzAlgorithm): """ def __init__(self): - super().__init__(PolyFuzz(method=TFIDF(min_similarity=0))) + super().__init__(PolyFuzz(method=TFIDF(n_gram_range=(1, 3), min_similarity=0))) class EditAlgorithm(PolyFuzzAlgorithm): From e55f070b677ce768663fed4f3ce7668db7382110 Mon Sep 17 00:00:00 2001 From: Roque Lopez Date: Tue, 25 Jun 2024 14:10:46 -0400 Subject: [PATCH 16/22] Change default algorithm for column mapping --- bdikit/functional_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bdikit/functional_api.py b/bdikit/functional_api.py index a98379a7..54111bfd 100644 --- a/bdikit/functional_api.py +++ b/bdikit/functional_api.py @@ -76,7 +76,7 @@ def get_instance(method_name: str) -> BaseColumnMappingAlgorithm: def match_columns( source: pd.DataFrame, target: Union[str, pd.DataFrame] = "gdc", - method: Union[str, BaseColumnMappingAlgorithm] = ColumnMappingMethod.SIMFLOOD.name, + method: Union[str, BaseColumnMappingAlgorithm] = "coma", ) -> pd.DataFrame: """ Performs schema mapping between the source table and the given target schema. The @@ -87,7 +87,7 @@ def match_columns( Parameters: source (pd.DataFrame): The source table to be mapped. target (Union[str, pd.DataFrame], optional): The target table or standard data vocabulary. Defaults to "gdc". - method (Union[str, BaseColumnMappingAlgorithm], optional): The method used for mapping. Defaults to ColumnMappingMethod.SIMFLOOD.name. + method (str, optional): The method used for mapping. Defaults to "coma". Returns: pd.DataFrame: A DataFrame containing the mapping results with columns "source" and "target". From b4f8fb18051f5a26b68ffae1b115475a5b3a7f0b Mon Sep 17 00:00:00 2001 From: Roque Lopez Date: Tue, 25 Jun 2024 14:52:22 -0400 Subject: [PATCH 17/22] Make EditAlgorithm to return values between 0 and 1 --- .../value_mapping/algorithms.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/bdikit/mapping_algorithms/value_mapping/algorithms.py b/bdikit/mapping_algorithms/value_mapping/algorithms.py index 6edbff39..c59db949 100644 --- a/bdikit/mapping_algorithms/value_mapping/algorithms.py +++ b/bdikit/mapping_algorithms/value_mapping/algorithms.py @@ -1,9 +1,10 @@ -from typing import List, NamedTuple +from typing import List, NamedTuple, Callable import ast from openai import OpenAI from polyfuzz import PolyFuzz from polyfuzz.models import EditDistance, TFIDF, Embeddings from flair.embeddings import TransformerWordEmbeddings, WordEmbeddings +from rapidfuzz import fuzz from autofj import AutoFJ from Levenshtein import ratio import pandas as pd @@ -76,8 +77,16 @@ class EditAlgorithm(PolyFuzzAlgorithm): Value matching algorithm based on the edit distance between values. """ - def __init__(self): - super().__init__(PolyFuzz(method=EditDistance(n_jobs=-1))) + def __init__(self, scorer: Callable[[str, str], float] = fuzz.ratio): + # Return scores between 0 and 1 + normalized_scorer = lambda str1, str2: scorer(str1, str2) / 100.0 + super().__init__( + PolyFuzz( + method=EditDistance( + n_jobs=-1, scorer=normalized_scorer, normalize=False + ) + ) + ) class EmbeddingAlgorithm(PolyFuzzAlgorithm): From 39fdfd9c7bc9a9b257a7d3bc638e9fb66cd27a5b Mon Sep 17 00:00:00 2001 From: Roque Lopez Date: Tue, 25 Jun 2024 15:08:51 -0400 Subject: [PATCH 18/22] Update threshold to fix test --- tests/test_value_matching_algorithms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_value_matching_algorithms.py b/tests/test_value_matching_algorithms.py index cb106105..ee4e6820 100644 --- a/tests/test_value_matching_algorithms.py +++ b/tests/test_value_matching_algorithms.py @@ -39,7 +39,7 @@ def test_edit_distance_value_matching(self): # when matches = edit_distance_matcher.match( - current_values, target_values, threshold=0.2 + current_values, target_values, threshold=0.5 ) # then @@ -52,4 +52,4 @@ def test_edit_distance_value_matching(self): self.assertEqual(mapped_matches["Oorange"][0], "orange") scores = [match[2] for match in matches] - self.assertTrue(all(score > 0.2 for score in scores)) + self.assertTrue(all(score > 0.5 for score in scores)) From be7c1bd5bb6bd2f45c3759633ce766706a776fb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=C3=A9cio=20Santos?= Date: Wed, 26 Jun 2024 18:05:54 -0400 Subject: [PATCH 19/22] refactor: Extract TopkColumnMatcher from ContrastiveLearningAPI This change will allows spliting the column embedding phase from the selection of top-k most similar embeddings. Besides allowing the introduction of alternative embedding models in the future, this will be allow the implementation of alternative TopkColumnMatcher's that instead of selecting the top-k based on embedding similarities, will return the results based on interactions of the user through with automatic recomendations through a Jupyter notebook and visualizations. --- bdikit/functional_api.py | 21 ++++--- .../column_mapping/algorithms.py | 54 ++++++++++------ .../column_mapping/topk_matchers.py | 61 +++++++++++++++++++ .../contrastive_learning/cl_api.py | 56 ++++++++++------- .../scope_reducing/algorithms.py | 9 +-- tests/test_column_mapping.py | 57 +++++++++-------- 6 files changed, 179 insertions(+), 79 deletions(-) create mode 100644 bdikit/mapping_algorithms/column_mapping/topk_matchers.py diff --git a/bdikit/functional_api.py b/bdikit/functional_api.py index 54111bfd..f098eb99 100644 --- a/bdikit/functional_api.py +++ b/bdikit/functional_api.py @@ -5,7 +5,6 @@ import pandas as pd import numpy as np from bdikit.utils import get_gdc_data -from bdikit.download import get_cached_model_or_download from bdikit.mapping_algorithms.column_mapping.algorithms import ( BaseColumnMappingAlgorithm, SimFloodAlgorithm, @@ -19,7 +18,10 @@ ) from bdikit.mapping_algorithms.value_mapping.value_mappers import ValueMapper from bdikit.mapping_algorithms.scope_reducing._algorithms.contrastive_learning.cl_api import ( - ContrastiveLearningAPI, + DEFAULT_CL_MODEL, +) +from bdikit.mapping_algorithms.column_mapping.topk_matchers import ( + CLTopkColumnMatcher, ) from bdikit.mapping_algorithms.value_mapping.algorithms import ( ValueMatch, @@ -145,17 +147,18 @@ def top_matches( else: selected_columns = source - model_path = get_cached_model_or_download("cl-reducer-v0.1") - api = ContrastiveLearningAPI(model_path=model_path, top_k=top_k) - _, scopes_json = api.get_recommendations(selected_columns, target=target_table) + topk_matcher = CLTopkColumnMatcher(model_name=DEFAULT_CL_MODEL) + top_k_matches = topk_matcher.get_recommendations( + selected_columns, target=target_table, top_k=top_k + ) dfs = [] - for scope in scopes_json: + for match in top_k_matches: matches = pd.DataFrame( - scope["Top k columns"], columns=["matches", "similarity"] + match["top_k_columns"], columns=["matches", "similarity"] ) - matches["source"] = scope["Candidate column"] - matches = matches[["source", "matches", "similarity"]] + matches["source"] = match["source_column"] + matches = matches[["source", "matches", "similarity"]] # reorder columns dfs.append(matches.sort_values(by="similarity", ascending=False)) return pd.concat(dfs, ignore_index=True) diff --git a/bdikit/mapping_algorithms/column_mapping/algorithms.py b/bdikit/mapping_algorithms/column_mapping/algorithms.py index 697cd8d7..f48438f9 100644 --- a/bdikit/mapping_algorithms/column_mapping/algorithms.py +++ b/bdikit/mapping_algorithms/column_mapping/algorithms.py @@ -12,9 +12,12 @@ from valentine.algorithms.matcher_results import MatcherResults from openai import OpenAI from bdikit.mapping_algorithms.scope_reducing._algorithms.contrastive_learning.cl_api import ( - ContrastiveLearningAPI, + DEFAULT_CL_MODEL, +) +from bdikit.mapping_algorithms.column_mapping.topk_matchers import ( + TopkColumnMatcher, + CLTopkColumnMatcher, ) -from bdikit.download import get_cached_model_or_download class BaseColumnMappingAlgorithm: @@ -94,7 +97,9 @@ def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame): break return self._fill_missing_matches(dataset, mappings) - def get_column_type(self, context, labels, m=10, model="gpt-4-turbo-preview"): + def get_column_type( + self, context: str, labels: str, m: int = 10, model: str = "gpt-4-turbo-preview" + ): messages = [ {"role": "system", "content": "You are an assistant for column matching."}, { @@ -117,46 +122,59 @@ def get_column_type(self, context, labels, m=10, model="gpt-4-turbo-preview"): class ContrastiveLearningAlgorithm(BaseColumnMappingAlgorithm): - def __init__(self, model_name: str = "cl-reducer-v0.1"): - model_path = get_cached_model_or_download(model_name) - self.api = ContrastiveLearningAPI(model_path=model_path, top_k=1) + def __init__(self, model_name: str = DEFAULT_CL_MODEL): + self.topk_matcher = CLTopkColumnMatcher(model_name=model_name) def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame): - union_scopes, scopes_json = self.api.get_recommendations(dataset) + topk_matches = self.topk_matcher.get_recommendations( + dataset, global_table, top_k=1 + ) matches = {} - for column, scope in zip(dataset.columns, scopes_json): - candidate = scope["Top k columns"][0][0] + for column, top_k_match in zip(dataset.columns, topk_matches): + candidate = top_k_match["top_k_columns"][0][0] if candidate in global_table.columns: matches[column] = candidate return self._fill_missing_matches(dataset, matches) class TwoPhaseMatcherAlgorithm(BaseColumnMappingAlgorithm): - def __init__(self, model_name: str = "cl-reducer-v0.1", top_k: int = 20): - model_path = get_cached_model_or_download(model_name) - self.api = ContrastiveLearningAPI(model_path=model_path, top_k=top_k) + + def __init__( + self, + top_k: int = 20, + top_k_matcher: TopkColumnMatcher = CLTopkColumnMatcher(DEFAULT_CL_MODEL), + schema_matcher: BaseColumnMappingAlgorithm = SimFloodAlgorithm(), + ): + self.api = top_k_matcher + self.schema_matcher = schema_matcher + self.top_k = top_k def map( self, dataset: pd.DataFrame, global_table: pd.DataFrame, - algorithm: BaseColumnMappingAlgorithm = SimFloodAlgorithm(), ): - union_scopes, scopes_json = self.api.get_recommendations(dataset) + topk_column_matches = self.api.get_recommendations( + dataset, global_table, self.top_k + ) + matches = {} - for column, scope in zip(dataset.columns, scopes_json): + for column, scope in zip(dataset.columns, topk_column_matches): candidates = [ cand[0] - for cand in scope["Top k columns"] + for cand in scope["top_k_columns"] if cand[0] in global_table.columns ] reduced_dataset = dataset[[column]] reduced_global_table = global_table[candidates] - partial_matches = algorithm.map(reduced_dataset, reduced_global_table) - + partial_matches = self.schema_matcher.map( + reduced_dataset, reduced_global_table + ) if len(partial_matches.keys()) > 0: candidate_col = next(iter(partial_matches)) target_col = partial_matches[candidate_col] matches[candidate_col] = target_col + if column in partial_matches: + matches[column] = partial_matches[column] return self._fill_missing_matches(dataset, matches) diff --git a/bdikit/mapping_algorithms/column_mapping/topk_matchers.py b/bdikit/mapping_algorithms/column_mapping/topk_matchers.py new file mode 100644 index 00000000..949fa9c8 --- /dev/null +++ b/bdikit/mapping_algorithms/column_mapping/topk_matchers.py @@ -0,0 +1,61 @@ +from typing import List, NamedTuple, TypedDict +import pandas as pd +import numpy as np +from sklearn.metrics.pairwise import cosine_similarity +from bdikit.mapping_algorithms.scope_reducing._algorithms.contrastive_learning.cl_api import ( + ContrastiveLearningAPI, + DEFAULT_CL_MODEL, +) + + +class ColumnScore(NamedTuple): + column_name: str + score: float + + +class TopkMatching(TypedDict): + source_column: str + top_k_columns: List[ColumnScore] + + +class TopkColumnMatcher: + + def get_recommendations( + self, source: pd.DataFrame, target: pd.DataFrame, top_k: int + ) -> List[TopkMatching]: # type: ignore + pass + + +class CLTopkColumnMatcher(TopkColumnMatcher): + def __init__(self, model_name: str = DEFAULT_CL_MODEL): + # TODO: we can generalize this api to accept any embedding model + # and not just our contrastive learning model + self.api = ContrastiveLearningAPI(model_name=model_name) + + def get_recommendations( + self, source: pd.DataFrame, target: pd.DataFrame, top_k: int = 10 + ) -> List[TopkMatching]: + """ + Returns the top-k matching columns in the target table for each column + in the source table. The ranking is based on the cosine similarity of + the embeddings of the columns in the source and target tables. + """ + l_features = self.api.get_embeddings(source) + r_features = self.api.get_embeddings(target) + cosine_sim = cosine_similarity(l_features, r_features) # type: ignore + + top_k_results = [] + for index, similarities in enumerate(cosine_sim): + top_k_indices = np.argsort(similarities)[::-1][:top_k] + top_k_columns = [ + ColumnScore(column_name=target.columns[i], score=similarities[i]) + for i in top_k_indices + ] + top_k_results.append( + { + "source_column": source.columns[index], + "top_k_columns": top_k_columns, + } + ) + + return top_k_results diff --git a/bdikit/mapping_algorithms/scope_reducing/_algorithms/contrastive_learning/cl_api.py b/bdikit/mapping_algorithms/scope_reducing/_algorithms/contrastive_learning/cl_api.py index 2a079b9d..308705ce 100644 --- a/bdikit/mapping_algorithms/scope_reducing/_algorithms/contrastive_learning/cl_api.py +++ b/bdikit/mapping_algorithms/scope_reducing/_algorithms/contrastive_learning/cl_api.py @@ -1,5 +1,5 @@ import os -from typing import List, Optional, Union +from typing import List, Dict, Tuple, Optional import numpy as np import pandas as pd @@ -12,22 +12,39 @@ ) from sklearn.metrics.pairwise import cosine_similarity from tqdm import tqdm +from bdikit.download import get_cached_model_or_download dir_path = os.path.dirname(os.path.realpath(__file__)) GDC_TABLE_PATH = os.path.join(dir_path, "../../../../resource/gdc_table.csv") -MODEL_PATH = os.path.join(dir_path, "../../../../resource/model_20_1.pt") +DEFAULT_CL_MODEL = "cl-reducer-v0.1" class ContrastiveLearningAPI: - def __init__(self, model_path=MODEL_PATH, top_k=10, batch_size=128): - self.model_path = model_path + def __init__( + self, + model_path: Optional[str] = None, + model_name: Optional[str] = None, + batch_size: int = 128, + ): + if model_name and model_path: + raise ValueError( + "Only one of model_name or model_path should be provided " + "(they are mutually exclusive)" + ) + + if model_path: + self.model_path = model_path + elif model_name: + self.model_path = get_cached_model_or_download(model_name) + else: + raise ValueError("Either model_name or model_path must be provided") + self.unlabeled = PretrainTableDataset() self.batch_size = batch_size self.device = "cuda" if torch.cuda.is_available() else "cpu" self.model = self.load_checkpoint() - self.top_k = top_k - def load_checkpoint(self, lm="roberta"): + def load_checkpoint(self, lm: str = "roberta"): ckpt = torch.load(self.model_path, map_location=torch.device("cpu")) scale_loss = 0.1 lambd = 3.9 @@ -37,28 +54,25 @@ def load_checkpoint(self, lm="roberta"): return model + def get_embeddings(self, table: pd.DataFrame) -> List[np.ndarray]: + return self._load_table_tokens(table) + def get_recommendations( - self, table: pd.DataFrame, target: Optional[Union[str, pd.DataFrame]] = None - ): - if target is None or (isinstance(target, str) and target == "gdc"): - gdc_ds = pd.read_csv(GDC_TABLE_PATH) - elif isinstance(target, pd.DataFrame): - gdc_ds = target - else: - raise ValueError("Target must be a DataFrame or 'gdc'") + self, table: pd.DataFrame, target: pd.DataFrame, top_k: int = 10 + ) -> Tuple[List, List[Dict]]: l_features = self._load_table_tokens(table) - r_features = self._load_table_tokens(gdc_ds) + r_features = self._load_table_tokens(target) cosine_sim = cosine_similarity(l_features, r_features) # print(f"l_features - {len(l_features)}:{l_features[0].shape}\nr-feature - {len(r_features)}:{r_features[0].shape}\nCosine - {cosine_sim.shape}") top_k_results = [] l_column_ids = table.columns - gt_column_ids = gdc_ds.columns + gt_column_ids = target.columns for index, similarities in enumerate(cosine_sim): - top_k_indices = np.argsort(similarities)[::-1][: self.top_k] + top_k_indices = np.argsort(similarities)[::-1][:top_k] top_k_column_names = [gt_column_ids[i] for i in top_k_indices] top_k_similarities = [str(round(similarities[i], 4)) for i in top_k_indices] top_k_columns = list(zip(top_k_column_names, top_k_similarities)) @@ -70,7 +84,7 @@ def get_recommendations( recommendations = self._extract_recommendations_from_top_k(top_k_results) return recommendations, top_k_results - def _extract_recommendations_from_top_k(self, top_k_results): + def _extract_recommendations_from_top_k(self, top_k_results: List[dict]): recommendations = set() for result in top_k_results: for name, _ in result["Top k columns"]: @@ -91,9 +105,9 @@ def _sample_to_15_rows(self, table: pd.DataFrame): table = unique_rows.sample(n=15, random_state=1) return table - def _load_table_tokens(self, table: pd.DataFrame): + def _load_table_tokens(self, table: pd.DataFrame) -> List[np.ndarray]: tables = [] - for i, column in enumerate(table.columns): + for _, column in enumerate(table.columns): curr_table = pd.DataFrame(table[column]) curr_table = self._sample_to_15_rows(curr_table) tables.append(curr_table) @@ -101,7 +115,7 @@ def _load_table_tokens(self, table: pd.DataFrame): print(f"Table features extracted from {len(table.columns)} columns") return [vec[-1] for vec in vectors] - def _inference_on_tables(self, tables: List[pd.DataFrame]): + def _inference_on_tables(self, tables: List[pd.DataFrame]) -> List[List]: total = len(tables) batch = [] results = [] diff --git a/bdikit/mapping_algorithms/scope_reducing/algorithms.py b/bdikit/mapping_algorithms/scope_reducing/algorithms.py index dd3fcd7f..05d749f7 100644 --- a/bdikit/mapping_algorithms/scope_reducing/algorithms.py +++ b/bdikit/mapping_algorithms/scope_reducing/algorithms.py @@ -1,8 +1,9 @@ import pandas as pd from bdikit.mapping_algorithms.scope_reducing._algorithms.contrastive_learning.cl_api import ( ContrastiveLearningAPI, + DEFAULT_CL_MODEL, + GDC_TABLE_PATH, ) -from bdikit.download import get_cached_model_or_download class BaseReducer: @@ -16,9 +17,9 @@ def reduce_scope(self, dataset: pd.DataFrame): class YurongReducer(BaseReducer): def __init__(self): super().__init__() - model_path = get_cached_model_or_download("cl-reducer-v0.1") - self.api = ContrastiveLearningAPI(model_path=model_path, top_k=20) + self.api = ContrastiveLearningAPI(model_name=DEFAULT_CL_MODEL) def reduce_scope(self, dataset: pd.DataFrame): - union_scopes, scopes_json = self.api.get_recommendations(dataset) + gdc_ds = pd.read_csv(GDC_TABLE_PATH) + _, scopes_json = self.api.get_recommendations(dataset, target=gdc_ds, top_k=20) return scopes_json diff --git a/tests/test_column_mapping.py b/tests/test_column_mapping.py index f8a04188..f1f00ffb 100644 --- a/tests/test_column_mapping.py +++ b/tests/test_column_mapping.py @@ -1,4 +1,3 @@ -import unittest import pandas as pd from bdikit.mapping_algorithms.column_mapping.algorithms import ( SimFloodAlgorithm, @@ -6,34 +5,38 @@ DistributionBasedAlgorithm, ComaAlgorithm, CupidAlgorithm, + TwoPhaseMatcherAlgorithm, + ContrastiveLearningAlgorithm, ) -class ColumnMappingTest(unittest.TestCase): - def test_basic_column_mapping_algorithms(self): - for ColumnMatcher in [ - SimFloodAlgorithm, - JaccardDistanceAlgorithm, - DistributionBasedAlgorithm, - ComaAlgorithm, - CupidAlgorithm, - ]: - # given - table1 = pd.DataFrame( - {"column_1": ["a1", "b1", "c1"], "col_2": ["a2", "b2", "c2"]} - ) - table2 = pd.DataFrame( - {"column_1a": ["a1", "b1", "c1"], "col2": ["a2", "b2", "c2"]} - ) - column_matcher = ColumnMatcher() +def test_basic_column_mapping_algorithms(): + for column_matcher in [ + SimFloodAlgorithm(), + JaccardDistanceAlgorithm(), + DistributionBasedAlgorithm(), + ComaAlgorithm(), + CupidAlgorithm(), + # + # Uncomment the following lines to test matchers that require + # downloading large models + # + # TwoPhaseMatcherAlgorithm(schema_matcher=ComaAlgorithm()), + # ContrastiveLearningAlgorithm() + ]: + # given + table1 = pd.DataFrame( + {"column_1": ["a1", "b1", "c1"], "col_2": ["a2", "b2", "c2"]} + ) + table2 = pd.DataFrame( + {"column_1a": ["a1", "b1", "c1"], "col2": ["a2", "b2", "c2"]} + ) - # when - mapping = column_matcher.map(dataset=table1, global_table=table2) + # when + mapping = column_matcher.map(dataset=table1, global_table=table2) - # then - print(mapping) - self.assertEqual( - {"column_1": "column_1a", "col_2": "col2"}, - mapping, - msg=f"{ColumnMatcher.__name__} failed to map columns", - ) + # then + assert ( + {"column_1": "column_1a", "col_2": "col2"} == mapping, + f"{type(column_matcher).__name__} failed to map columns", + ) From 154f5e90d9fc5a5bda9cdfe1702caf74fb0b5d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=C3=A9cio=20Santos?= Date: Wed, 26 Jun 2024 20:29:01 -0400 Subject: [PATCH 20/22] Additional documentation and implementation improvements --- bdikit/functional_api.py | 94 ++++++++++++++----- .../column_mapping/algorithms.py | 20 ++-- .../value_mapping/algorithms.py | 2 +- tests/test_api.py | 18 ++-- tests/test_column_mapping.py | 12 +-- 5 files changed, 101 insertions(+), 45 deletions(-) diff --git a/bdikit/functional_api.py b/bdikit/functional_api.py index f098eb99..874a87ba 100644 --- a/bdikit/functional_api.py +++ b/bdikit/functional_api.py @@ -42,6 +42,8 @@ GDC_DATA_PATH = join(dirname(__file__), "./resource/gdc_table.csv") +DEFAULT_VALUE_MATCHING_METHOD = "tfidf" +DEFAULT_SCHEMA_MATCHING_METHOD = "coma" class ColumnMappingMethod(Enum): @@ -78,7 +80,7 @@ def get_instance(method_name: str) -> BaseColumnMappingAlgorithm: def match_columns( source: pd.DataFrame, target: Union[str, pd.DataFrame] = "gdc", - method: Union[str, BaseColumnMappingAlgorithm] = "coma", + method: Union[str, BaseColumnMappingAlgorithm] = DEFAULT_SCHEMA_MATCHING_METHOD, ) -> pd.DataFrame: """ Performs schema mapping between the source table and the given target schema. The @@ -135,6 +137,15 @@ def top_matches( ) -> pd.DataFrame: """ Returns the top-k matches between the source and target tables. + + Args: + source (pd.DataFrame): The source table. + columns (Optional[List[str]], optional): The list of columns to consider for matching. Defaults to None. + target (Union[str, pd.DataFrame], optional): The target table or the name of the standard target table. Defaults to "gdc". + top_k (int, optional): The number of top matches to return. Defaults to 10. + + Returns: + pd.DataFrame: A DataFrame containing the top-k matches between the source and target tables. """ if isinstance(target, str): @@ -154,11 +165,9 @@ def top_matches( dfs = [] for match in top_k_matches: - matches = pd.DataFrame( - match["top_k_columns"], columns=["matches", "similarity"] - ) + matches = pd.DataFrame(match["top_k_columns"], columns=["target", "similarity"]) matches["source"] = match["source_column"] - matches = matches[["source", "matches", "similarity"]] # reorder columns + matches = matches[["source", "target", "similarity"]] # reorder columns dfs.append(matches.sort_values(by="similarity", ascending=False)) return pd.concat(dfs, ignore_index=True) @@ -246,11 +255,16 @@ def match_values( source: pd.DataFrame, target: Union[str, pd.DataFrame], column_mapping: pd.DataFrame, - method: str = ValueMatchingMethod.EDIT.name, + method: str = DEFAULT_VALUE_MATCHING_METHOD, ) -> List[ValueMatchingResult]: """ Maps the values of the dataset columns to the target domain using the given method name. """ + if not all(k in column_mapping.columns for k in ["source", "target"]): + raise ValueError( + "The column_mapping DataFrame must contain 'source' and 'target' columns." + ) + if isinstance(target, str) and target == "gdc": column_names = column_mapping["target"].unique().tolist() target_domain = get_gdc_data(column_names) @@ -346,7 +360,7 @@ def preview_value_mappings( dataset: pd.DataFrame, column_mapping: Union[Tuple[str, str], pd.DataFrame], target: Union[str, pd.DataFrame] = "gdc", - method: str = "edit_distance", + method: str = "tfidf", ) -> List[Dict]: """ Print the value mappings in a human-readable format. @@ -385,8 +399,8 @@ def preview_value_mappings( data=list( zip( unmatched_values, - [""] * len(unmatched_values), - [""] * len(unmatched_values), + [None] * len(unmatched_values), + [None] * len(unmatched_values), ) ), columns=["source", "target", "similarity"], @@ -413,10 +427,11 @@ def preview_domains( dataset: pd.DataFrame, column_mapping: Tuple[str, str], target: Union[str, pd.DataFrame] = "gdc", - limit: int = 10, + limit: Optional[int] = None, ) -> pd.DataFrame: """ - Preview the domain of the given columns in the source and target datasets. + Preview the domain (set of unique values) of the given columns in the source and target + dataset (or target data dictionary). Args: dataset (pd.DataFrame): The source dataset containing the columns to preview. @@ -424,22 +439,27 @@ def preview_domains( The first and second positions should contain the names of the source and target columns respectively. target (Union[str, pd.DataFrame], optional): The target dataset or standard vocabulary name. - If a string is provided and it is equal to "gdc", the target domain will be retrieved from the GDC data. + If a string is provided and it is equal to "gdc", the target domain will be retrieved + from the GDC data. If a DataFrame is provided, the target domain will be retrieved from the specified DataFrame. Defaults to "gdc". limit (int, optional): The maximum number of unique values to include in the preview. Defaults to 10. Returns: - pd.DataFrame: A DataFrame containing a sample of the source and target domain values. - The DataFrame will have two columns: "source_domain_sample" and "target_domain_sample". + pd.DataFrame: A DataFrame containing the source and target domain values (or a sample of + them if the parameter `limit` was specified). The DataFrame will have two columns: + "source_domain" and "target_domain". """ source_column, target_column = column_mapping source_domain = dataset[source_column].unique() if isinstance(target, str) and target == "gdc": - target_domain = np.array(get_gdc_data([target_column])[target_column]) + gdc_col_domain = get_gdc_data([target_column])[target_column] + target_domain = ( + np.array([]) if gdc_col_domain is None else np.array(gdc_col_domain) + ) elif isinstance(target, pd.DataFrame): target_domain = target[target_column].unique() else: @@ -447,13 +467,19 @@ def preview_domains( "The target must be a DataFrame or a standard vocabulary name." ) - output_size = min(max(len(source_domain), len(target_domain)), limit) + # Find the final output size based on the the largest domain size and limit parameter + largest_domain_size = max(len(source_domain), len(target_domain)) + output_size = ( + largest_domain_size if limit is None else min(largest_domain_size, limit) + ) + # Truncate the domains to the output size if they are larger if len(source_domain) > output_size: source_domain = source_domain[:output_size] if len(target_domain) > output_size: target_domain = target_domain[:output_size] + # Fill the domains with empty strings if they are smaller than the output size if len(source_domain) < output_size: source_domain = np.append( source_domain, np.full(output_size - len(source_domain), "") @@ -464,7 +490,7 @@ def preview_domains( ) return pd.DataFrame( - {"source_domain_sample": source_domain, "target_domain_sample": target_domain} + {"source_domain": source_domain, "target_domain": target_domain} ) @@ -472,14 +498,38 @@ def preview_domains( def update_mappings( - value_mappings: ValueMatchingLike, user_mappings: Optional[ValueMatchingLike] = None + mappings: ValueMatchingLike, user_mappings: Optional[ValueMatchingLike] = None ) -> List: + """ + Creates a "data harmonization" plan based on provide schema or value mappings. + These mappings can either be computed the library's functions or provided by the user. + If the user mappings are provided (using the user_mappings parameter), they will take + precedence over the mappings provided in ther first parameter. + + Args: + value_mappings (ValueMatchingLike): The value mappings used to create the data + harmonization plan. It can be a pandas DataFrame or a list of dictionaries + (ValueMatchingResult). + user_mappings (Optional[ValueMatchingLike]): The user mappings to be included in + the update. It can be a pandas DataFrame or a list of dictionaries (ValueMatchingResult). + Defaults to None. + + Returns: + List: The data harmonization plan that can be used as input to the materialize_mappings() + function. Concretely, the harmonization plan is a list of dictionaries, where each + dictionary contains the source column, target column, and mapper object that will be used + to transform the input to the output data. + + Raises: + ValueError: If there are duplicate mappings for the same source and target columns. + + """ if user_mappings is None: user_mappings = [] - if isinstance(value_mappings, pd.DataFrame): - value_mappings = value_mappings.to_dict(orient="records") + if isinstance(mappings, pd.DataFrame): + mappings = mappings.to_dict(orient="records") if isinstance(user_mappings, pd.DataFrame): user_mappings = user_mappings.to_dict(orient="records") @@ -499,13 +549,13 @@ def check_duplicates(mappings: List): # first check duplicates in each individual list check_duplicates(user_mappings) - check_duplicates(value_mappings) + check_duplicates(mappings) mapping_keys = set() final_mappings = [] # include all unique user mappings first, as they take precedence - for mapping in itertools.chain(user_mappings, value_mappings): + for mapping in itertools.chain(user_mappings, mappings): source_column = mapping["source"] target_column = mapping["target"] diff --git a/bdikit/mapping_algorithms/column_mapping/algorithms.py b/bdikit/mapping_algorithms/column_mapping/algorithms.py index f48438f9..aeb71dfd 100644 --- a/bdikit/mapping_algorithms/column_mapping/algorithms.py +++ b/bdikit/mapping_algorithms/column_mapping/algorithms.py @@ -1,5 +1,5 @@ import pandas as pd -from typing import Dict +from typing import Dict, Optional from valentine import valentine_match from valentine.algorithms import ( SimilarityFlooding, @@ -142,10 +142,19 @@ class TwoPhaseMatcherAlgorithm(BaseColumnMappingAlgorithm): def __init__( self, top_k: int = 20, - top_k_matcher: TopkColumnMatcher = CLTopkColumnMatcher(DEFAULT_CL_MODEL), + top_k_matcher: Optional[TopkColumnMatcher] = None, schema_matcher: BaseColumnMappingAlgorithm = SimFloodAlgorithm(), ): - self.api = top_k_matcher + if top_k_matcher is None: + self.api = CLTopkColumnMatcher(DEFAULT_CL_MODEL) + elif isinstance(top_k_matcher, TopkColumnMatcher): + self.api = top_k_matcher + else: + raise ValueError( + f"Invalid top_k_matcher type: {type(top_k_matcher)}. " + "Must be a subclass of {TopkColumnMatcher.__name__}" + ) + self.schema_matcher = schema_matcher self.top_k = top_k @@ -170,10 +179,7 @@ def map( partial_matches = self.schema_matcher.map( reduced_dataset, reduced_global_table ) - if len(partial_matches.keys()) > 0: - candidate_col = next(iter(partial_matches)) - target_col = partial_matches[candidate_col] - matches[candidate_col] = target_col + if column in partial_matches: matches[column] = partial_matches[column] diff --git a/bdikit/mapping_algorithms/value_mapping/algorithms.py b/bdikit/mapping_algorithms/value_mapping/algorithms.py index c59db949..92fdd4bc 100644 --- a/bdikit/mapping_algorithms/value_mapping/algorithms.py +++ b/bdikit/mapping_algorithms/value_mapping/algorithms.py @@ -45,7 +45,7 @@ def match( self, current_values: List[str], target_values: List[str], - threshold: float = 0.8, + threshold: float = 0.25, ) -> List[ValueMatch]: self.model.match(current_values, target_values) diff --git a/tests/test_api.py b/tests/test_api.py index 53cb1a77..b9bc601b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -80,13 +80,13 @@ def test_bdi_top_matches_with_dataframes(): # then assert len(df_matches.index) == 3 assert "source" in df_matches.columns - assert "matches" in df_matches.columns + assert "target" in df_matches.columns assert "similarity" in df_matches.columns df_filter = df_matches["source"] == "tumor_size" - assert "tumor_size" in df_matches[df_filter]["matches"].tolist() - assert "tumor_magnitude" in df_matches[df_filter]["matches"].tolist() - assert "tumor_length" in df_matches[df_filter]["matches"].tolist() + assert "tumor_size" in df_matches[df_filter]["target"].tolist() + assert "tumor_magnitude" in df_matches[df_filter]["target"].tolist() + assert "tumor_length" in df_matches[df_filter]["target"].tolist() def test_bdi_top_matches_gdc(): @@ -116,18 +116,18 @@ def test_bdi_top_matches_gdc(): # then assert df_matches.empty == False assert "source" in df_matches.columns - assert "matches" in df_matches.columns + assert "target" in df_matches.columns assert "similarity" in df_matches.columns df_filter = df_matches["source"] == "FIGO_stage" assert len(df_matches[df_filter]) == 5 - assert "figo_stage" in df_matches[df_filter]["matches"].tolist() - assert "uicc_clinical_stage" in df_matches[df_filter]["matches"].tolist() + assert "figo_stage" in df_matches[df_filter]["target"].tolist() + assert "uicc_clinical_stage" in df_matches[df_filter]["target"].tolist() df_filter = df_matches["source"] == "Ethnicity" assert len(df_matches[df_filter]) == 5 - assert "ethnicity" in df_matches[df_filter]["matches"].tolist() - assert "race" in df_matches[df_filter]["matches"].tolist() + assert "ethnicity" in df_matches[df_filter]["target"].tolist() + assert "race" in df_matches[df_filter]["target"].tolist() def test_map_column_values(): diff --git a/tests/test_column_mapping.py b/tests/test_column_mapping.py index f1f00ffb..61dd2dd3 100644 --- a/tests/test_column_mapping.py +++ b/tests/test_column_mapping.py @@ -21,8 +21,8 @@ def test_basic_column_mapping_algorithms(): # Uncomment the following lines to test matchers that require # downloading large models # - # TwoPhaseMatcherAlgorithm(schema_matcher=ComaAlgorithm()), - # ContrastiveLearningAlgorithm() + TwoPhaseMatcherAlgorithm(schema_matcher=ComaAlgorithm()), + ContrastiveLearningAlgorithm(), ]: # given table1 = pd.DataFrame( @@ -36,7 +36,7 @@ def test_basic_column_mapping_algorithms(): mapping = column_matcher.map(dataset=table1, global_table=table2) # then - assert ( - {"column_1": "column_1a", "col_2": "col2"} == mapping, - f"{type(column_matcher).__name__} failed to map columns", - ) + assert { + "column_1": "column_1a", + "col_2": "col2", + } == mapping, f"{type(column_matcher).__name__} failed to map columns" From 5f65b6271ebc1fae97ee36a15911fee961372dec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=C3=A9cio=20Santos?= Date: Wed, 26 Jun 2024 20:37:19 -0400 Subject: [PATCH 21/22] Rename notebook to doc_gdc_harmonization.ipynb --- ...rted.ipynb => dou_gdc_harmonization.ipynb} | 2951 +++++++++-------- 1 file changed, 1633 insertions(+), 1318 deletions(-) rename examples/{getting_started.ipynb => dou_gdc_harmonization.ipynb} (78%) diff --git a/examples/getting_started.ipynb b/examples/dou_gdc_harmonization.ipynb similarity index 78% rename from examples/getting_started.ipynb rename to examples/dou_gdc_harmonization.ipynb index f45b5600..c911f29e 100644 --- a/examples/getting_started.ipynb +++ b/examples/dou_gdc_harmonization.ipynb @@ -49,12 +49,12 @@ "data": { "application/vnd.holoviews_exec.v0+json": "", "text/html": [ - "
\n", - "
\n", + "
\n", + "
\n", "
\n", "