Repository: VIDA-NYU/bdi-kit (language: python)

Issue:
New data integration API
We'll use this issue to track the implementation of the new bdi-kit API.

Implementation:
- [x] `bdi.match_columns()`
- [x] `bdi.top_matches()`
- [x] `bdi.preview_domains()`
- [x] `bdi.preview_value_matches()`
- [x] `bdi.update_matches()`
- [x] `bdi.match_values()`
- [x] `bdi.materialize_mapping()`

Documentation:
- [ ] Make sure that all public functions have pydocs
- [ ] Make sure that readthedocs is generating API documentation
- [ ] Create a notebook demonstrating the API usage
Adding TwoPhase ColumnMatch algorithm based on CTLearning, Fix case where MatchAlgs do not find matches

TFIDFAlgorithm doesn't work for short values
I got the error below for these lists `source = ['G1', 'G2', 'G3', 'G4'] and target = ['G1', 'G2', 'G3', 'G4']`. By default, PolyFuzz generates [character-based n-grams of length 3](https://github.com/MaartenGr/PolyFuzz/blob/5d0734b093ece13e3e0e95dd3a26549f014497a8/polyfuzz/models/_tfidf.py#L50). Changing this to `TFIDF(n_gram_range=(1, 3), min_similarity=0)` fix the problems.
```
File ~/ASKEM/bdi-kit/bdikit/mapping_algorithms/value_mapping/algorithms.py:50, in PolyFuzzAlgorithm.match(self, current_values, target_values, threshold)
     43 def match(
     44     self,
     45     current_values: List[str],
     46     target_values: List[str],
     47     threshold: float = 0.8,
     48 ) -> List[ValueMatch]:
---> 50     self.model.match(current_values, target_values)
     51     match_results = self.model.get_matches()
     52     match_results.sort_values(by="Similarity", ascending=False, inplace=True)

File ~/opt/anaconda3/envs/askem/lib/python3.10/site-packages/polyfuzz/polyfuzz.py:143, in PolyFuzz.match(self, from_list, to_list, top_n)
    141 # Custom models
    142 elif isinstance(self.method, BaseMatcher):
--> 143     self.matches = {self.method.model_id: self.method.match(from_list, to_list)}
    144     logging.info(f"Ran model with model id = {self.method.model_id}")
    146 # Multiple custom models

File ~/opt/anaconda3/envs/askem/lib/python3.10/site-packages/polyfuzz/models/_tfidf.py:90, in TFIDF.match(self, from_list, to_list, re_train)
     65 def match(self,
     66           from_list: List[str],
     67           to_list: List[str] = None,
     68           re_train: bool = True) -> pd.DataFrame:
     69     """ Match two lists of strings to each other and return the most similar strings
     70 
     71     Arguments:
   (...)
     87     ```
     88     """
---> 90     tf_idf_from, tf_idf_to = self._extract_tf_idf(from_list, to_list, re_train)
     91     matches = cosine_similarity(tf_idf_from, tf_idf_to,
     92                                 from_list, to_list,
     93                                 self.min_similarity,
     94                                 top_n=self.top_n,
     95                                 method=self.cosine_method)
     97     return matches

File ~/opt/anaconda3/envs/askem/lib/python3.10/site-packages/polyfuzz/models/_tfidf.py:106, in TFIDF._extract_tf_idf(self, from_list, to_list, re_train)
    104 if to_list:
    105     if re_train:
--> 106         self.vectorizer = TfidfVectorizer(min_df=1, analyzer=self._create_ngrams).fit(to_list + from_list)
    107         self.tf_idf_to = self.vectorizer.transform(to_list)
    108     tf_idf_from = self.vectorizer.transform(from_list)

File ~/opt/anaconda3/envs/askem/lib/python3.10/site-packages/sklearn/base.py:1474, in _fit_context.<locals>.decorator.<locals>.wrapper(estimator, *args, **kwargs)
   1467     estimator._validate_params()
   1469 with config_context(
   1470     skip_parameter_validation=(
   1471         prefer_skip_nested_validation or global_skip_validation
   1472     )
   1473 ):
-> 1474     return fit_method(estimator, *args, **kwargs)

File ~/opt/anaconda3/envs/askem/lib/python3.10/site-packages/sklearn/feature_extraction/text.py:2108, in TfidfVectorizer.fit(self, raw_documents, y)
   2101 self._warn_for_unused_params()
   2102 self._tfidf = TfidfTransformer(
   2103     norm=self.norm,
   2104     use_idf=self.use_idf,
   2105     smooth_idf=self.smooth_idf,
   2106     sublinear_tf=self.sublinear_tf,
   2107 )
-> 2108 X = super().fit_transform(raw_documents)
   2109 self._tfidf.fit(X)
   2110 return self

File ~/opt/anaconda3/envs/askem/lib/python3.10/site-packages/sklearn/base.py:1474, in _fit_context.<locals>.decorator.<locals>.wrapper(estimator, *args, **kwargs)
   1467     estimator._validate_params()
   1469 with config_context(
   1470     skip_parameter_validation=(
   1471         prefer_skip_nested_validation or global_skip_validation
   1472     )
   1473 ):
-> 1474     return fit_method(estimator, *args, **kwargs)

File ~/opt/anaconda3/envs/askem/lib/python3.10/site-packages/sklearn/feature_extraction/text.py:1389, in CountVectorizer.fit_transform(self, raw_documents, y)
   1381             warnings.warn(
   1382                 "Upper case characters found in"
   1383                 " vocabulary while 'lowercase'"
   1384                 " is True. These entries will not"
   1385                 " be matched with any documents"
   1386             )
   1387             break
-> 1389 vocabulary, X = self._count_vocab(raw_documents, self.fixed_vocabulary_)
   1391 if self.binary:
   1392     X.data.fill(1)

File ~/opt/anaconda3/envs/askem/lib/python3.10/site-packages/sklearn/feature_extraction/text.py:1295, in CountVectorizer._count_vocab(self, raw_documents, fixed_vocab)
   1293     vocabulary = dict(vocabulary)
   1294     if not vocabulary:
-> 1295         raise ValueError(
   1296             "empty vocabulary; perhaps the documents only contain stop words"
   1297         )
   1299 if indptr[-1] > np.iinfo(np.int32).max:  # = 2**31 - 1
   1300     if _IS_32BIT:

ValueError: empty vocabulary; perhaps the documents only contain stop words```


Discussion / hints:

Fix patch:
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..4b01caf3
--- /dev/null
+++ b/bdikit/functional_api.py
@@ -0,0 +1,684 @@
+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
+from bdikit.mapping_algorithms.column_mapping.algorithms import (
+    BaseColumnMappingAlgorithm,
+    SimFloodAlgorithm,
+    ComaAlgorithm,
+    CupidAlgorithm,
+    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 (
+    DEFAULT_CL_MODEL,
+)
+from bdikit.mapping_algorithms.column_mapping.topk_matchers import (
+    CLTopkColumnMatcher,
+)
+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")
+DEFAULT_VALUE_MATCHING_METHOD = "tfidf"
+DEFAULT_SCHEMA_MATCHING_METHOD = "coma"
+
+
+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)
+    CT_LEARGNING = ("ct_learning", ContrastiveLearningAlgorithm)
+    TWO_PHASE = ("two_phase", TwoPhaseMatcherAlgorithm)
+
+    def __init__(
+        self, method_name: str, method_class: Type[BaseColumnMappingAlgorithm]
+    ):
+        self.method_name = method_name
+        self.method_class = method_class
+
+    @staticmethod
+    def get_instance(method_name: str) -> BaseColumnMappingAlgorithm:
+        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: Union[str, BaseColumnMappingAlgorithm] = DEFAULT_SCHEMA_MATCHING_METHOD,
+) -> pd.DataFrame:
+    """
+    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 (str, optional): The method used for mapping. Defaults to "coma".
+
+    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
+
+    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"])
+
+
+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")
+
+
+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.
+
+    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):
+        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
+
+    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 match in top_k_matches:
+        matches = pd.DataFrame(match["top_k_columns"], columns=["target", "similarity"])
+        matches["source"] = match["source_column"]
+        matches = matches[["source", "target", "similarity"]]  # reorder columns
+        dfs.append(matches.sort_values(by="similarity", ascending=False))
+
+    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_table: pd.DataFrame, mapping_spec: Union[List[dict], pd.DataFrame]
+) -> 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.
+    """
+    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"]
+        to_column_name = column_spec["target"]
+        value_mapper = column_spec["mapper"]
+        output_dataframe[to_column_name] = map_column_values(
+            input_table[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
+
+
+class ValueMatchingResult(TypedDict):
+    source: str
+    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 = 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)
+    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,
+) -> List[ValueMatchingResult]:
+
+    mapping_results: List[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] = {
+            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.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
+
+
+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 = "tfidf",
+) -> 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 matching_result in value_mappings:
+
+        # 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,
+                    [None] * len(unmatched_values),
+                    [None] * len(unmatched_values),
+                )
+            ),
+            columns=["source", "target", "similarity"],
+        )
+
+        result.append(
+            {
+                "source": matching_result["source"],
+                "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 preview_domains(
+    dataset: pd.DataFrame,
+    column_mapping: Tuple[str, str],
+    target: Union[str, pd.DataFrame] = "gdc",
+    limit: Optional[int] = None,
+) -> pd.DataFrame:
+    """
+    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.
+        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 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":
+        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:
+        raise ValueError(
+            "The target must be a DataFrame or a standard vocabulary name."
+        )
+
+    # 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), "")
+        )
+    if len(target_domain) < output_size:
+        target_domain = np.append(
+            target_domain, np.full(output_size - len(target_domain), "")
+        )
+
+    return pd.DataFrame(
+        {"source_domain": source_domain, "target_domain": target_domain}
+    )
+
+
+ValueMatchingLike = Union[List[ValueMatchingResult], List[Dict], pd.DataFrame]
+
+
+def update_mappings(
+    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:
+        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(mappings, pd.DataFrame):
+        mappings = 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(mappings)
+
+    mapping_keys = set()
+    final_mappings = []
+
+    # include all unique user mappings first, as they take precedence
+    for mapping in itertools.chain(user_mappings, 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,
+        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 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 (<source_value>, <target_value>),
+      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)
+
+    # 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/bdikit/mapping_algorithms/column_mapping/algorithms.py b/bdikit/mapping_algorithms/column_mapping/algorithms.py
index 4ac5db56..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,
@@ -11,71 +11,78 @@
 )
 from valentine.algorithms.matcher_results import MatcherResults
 from openai import OpenAI
+from bdikit.mapping_algorithms.scope_reducing._algorithms.contrastive_learning.cl_api import (
+    DEFAULT_CL_MODEL,
+)
+from bdikit.mapping_algorithms.column_mapping.topk_matchers import (
+    TopkColumnMatcher,
+    CLTopkColumnMatcher,
+)
 
 
 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")
 
+    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, 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]
             global_table_candidate = match[1][1]
             mappings[dataset_candidate] = global_table_candidate
-        return mappings
+        return self._fill_missing_matches(dataset, mappings)
 
 
 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()
@@ -88,9 +95,11 @@ def map(self):
                 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"):
+    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."},
             {
@@ -110,3 +119,68 @@ 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_name: str = DEFAULT_CL_MODEL):
+        self.topk_matcher = CLTopkColumnMatcher(model_name=model_name)
+
+    def map(self, dataset: pd.DataFrame, global_table: pd.DataFrame):
+        topk_matches = self.topk_matcher.get_recommendations(
+            dataset, global_table, top_k=1
+        )
+        matches = {}
+        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,
+        top_k: int = 20,
+        top_k_matcher: Optional[TopkColumnMatcher] = None,
+        schema_matcher: BaseColumnMappingAlgorithm = SimFloodAlgorithm(),
+    ):
+        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
+
+    def map(
+        self,
+        dataset: pd.DataFrame,
+        global_table: pd.DataFrame,
+    ):
+        topk_column_matches = self.api.get_recommendations(
+            dataset, global_table, self.top_k
+        )
+
+        matches = {}
+        for column, scope in zip(dataset.columns, topk_column_matches):
+            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 = self.schema_matcher.map(
+                reduced_dataset, reduced_global_table
+            )
+
+            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 4d7d10b6..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
+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,20 +54,25 @@ 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_embeddings(self, table: pd.DataFrame) -> List[np.ndarray]:
+        return self._load_table_tokens(table)
+
+    def get_recommendations(
+        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))
@@ -62,7 +84,7 @@ def get_recommendations(self, table: pd.DataFrame):
         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"]:
@@ -83,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)
@@ -93,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/bdikit/mapping_algorithms/value_mapping/algorithms.py b/bdikit/mapping_algorithms/value_mapping/algorithms.py
index 0e0f26b1..92fdd4bc 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
+from flair.embeddings import TransformerWordEmbeddings, WordEmbeddings
+from rapidfuzz import fuzz
 from autofj import AutoFJ
 from Levenshtein import ratio
 import pandas as pd
@@ -44,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)
@@ -68,7 +69,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):
@@ -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):
@@ -91,6 +100,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
new file mode 100644
index 00000000..d6ad11d3
--- /dev/null
+++ b/bdikit/mapping_algorithms/value_mapping/value_mappers.py
@@ -0,0 +1,64 @@
+import pandas as pd
+from typing import Any, Callable
+from collections import defaultdict
+
+
+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: Callable[[pd.Series], pd.Series]):
+        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, 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, na_action="ignore")
diff --git a/docs/source/api.rst b/docs/source/api.rst
index fe4f7256..0f0ba5b5 100644
--- a/docs/source/api.rst
+++ b/docs/source/api.rst
@@ -1,7 +1,7 @@
 API
-=========
+====
 
-.. module:: bdikit.api
 
-.. autoclass:: APIManager
-   :members:
+.. automodule:: bdikit.functional_api
+    :members:
+    :exclude-members: ColumnMappingMethod, ValueMatchingMethod, ValueMatchingResult
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 3432b19e..0c4def98 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -33,6 +33,7 @@
 # ones.
 extensions = [
     "sphinx.ext.autodoc",
+    "sphinx.ext.napoleon",
     "nbsphinx",
     "nbsphinx_link",
     "sphinxemoji.sphinxemoji",
@@ -88,5 +89,6 @@
     "valentine",
     "altair",
     "panel",
-    "tqdm"
+    "tqdm",
+    "rapidfuzz",
 ]
diff --git a/examples/dou_gdc_harmonization.ipynb b/examples/dou_gdc_harmonization.ipynb
new file mode 100644
index 00000000..c911f29e
--- /dev/null
+++ b/examples/dou_gdc_harmonization.ipynb
@@ -0,0 +1,3426 @@
+{
+ "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": [
+       "<style>*[data-root-id],\n",
+       "*[data-root-id] > * {\n",
+       "  box-sizing: border-box;\n",
+       "  font-family: var(--jp-ui-font-family);\n",
+       "  font-size: var(--jp-ui-font-size1);\n",
+       "  color: var(--vscode-editor-foreground, var(--jp-ui-font-color1));\n",
+       "}\n",
+       "\n",
+       "/* Override VSCode background color */\n",
+       ".cell-output-ipywidget-background:has(\n",
+       "    > .cell-output-ipywidget-background > .lm-Widget > *[data-root-id]\n",
+       "  ),\n",
+       ".cell-output-ipywidget-background:has(> .lm-Widget > *[data-root-id]) {\n",
+       "  background-color: transparent !important;\n",
+       "}\n",
+       "</style>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.holoviews_exec.v0+json": "",
+      "text/html": [
+       "<div id='8eee4fd3-3c98-4841-a231-c5a6bffcd827'>\n",
+       "  <div id=\"e557a804-4019-4291-9768-a9b903ba35af\" data-root-id=\"8eee4fd3-3c98-4841-a231-c5a6bffcd827\" style=\"display: contents;\"></div>\n",
+       "</div>\n",
+       "<script type=\"application/javascript\">(function(root) {\n",
+       "  var docs_json = {\"891adebc-bb6a-44f8-be06-8f653d5b4253\":{\"version\":\"3.1.1\",\"title\":\"Bokeh Application\",\"defs\":[{\"type\":\"model\",\"name\":\"ReactiveHTML1\"},{\"type\":\"model\",\"name\":\"FlexBox1\",\"properties\":[{\"name\":\"align_content\",\"kind\":\"Any\",\"default\":\"flex-start\"},{\"name\":\"align_items\",\"kind\":\"Any\",\"default\":\"flex-start\"},{\"name\":\"flex_direction\",\"kind\":\"Any\",\"default\":\"row\"},{\"name\":\"flex_wrap\",\"kind\":\"Any\",\"default\":\"wrap\"},{\"name\":\"justify_content\",\"kind\":\"Any\",\"default\":\"flex-start\"}]},{\"type\":\"model\",\"name\":\"FloatPanel1\",\"properties\":[{\"name\":\"config\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}},{\"name\":\"contained\",\"kind\":\"Any\",\"default\":true},{\"name\":\"position\",\"kind\":\"Any\",\"default\":\"right-top\"},{\"name\":\"offsetx\",\"kind\":\"Any\",\"default\":null},{\"name\":\"offsety\",\"kind\":\"Any\",\"default\":null},{\"name\":\"theme\",\"kind\":\"Any\",\"default\":\"primary\"},{\"name\":\"status\",\"kind\":\"Any\",\"default\":\"normalized\"}]},{\"type\":\"model\",\"name\":\"GridStack1\",\"properties\":[{\"name\":\"mode\",\"kind\":\"Any\",\"default\":\"warn\"},{\"name\":\"ncols\",\"kind\":\"Any\",\"default\":null},{\"name\":\"nrows\",\"kind\":\"Any\",\"default\":null},{\"name\":\"allow_resize\",\"kind\":\"Any\",\"default\":true},{\"name\":\"allow_drag\",\"kind\":\"Any\",\"default\":true},{\"name\":\"state\",\"kind\":\"Any\",\"default\":[]}]},{\"type\":\"model\",\"name\":\"drag1\",\"properties\":[{\"name\":\"slider_width\",\"kind\":\"Any\",\"default\":5},{\"name\":\"slider_color\",\"kind\":\"Any\",\"default\":\"black\"},{\"name\":\"value\",\"kind\":\"Any\",\"default\":50}]},{\"type\":\"model\",\"name\":\"click1\",\"properties\":[{\"name\":\"terminal_output\",\"kind\":\"Any\",\"default\":\"\"},{\"name\":\"debug_name\",\"kind\":\"Any\",\"default\":\"\"},{\"name\":\"clears\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"FastWrapper1\",\"properties\":[{\"name\":\"object\",\"kind\":\"Any\",\"default\":null},{\"name\":\"style\",\"kind\":\"Any\",\"default\":null}]},{\"type\":\"model\",\"name\":\"NotificationAreaBase1\",\"properties\":[{\"name\":\"js_events\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}},{\"name\":\"position\",\"kind\":\"Any\",\"default\":\"bottom-right\"},{\"name\":\"_clear\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"NotificationArea1\",\"properties\":[{\"name\":\"js_events\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}},{\"name\":\"notifications\",\"kind\":\"Any\",\"default\":[]},{\"name\":\"position\",\"kind\":\"Any\",\"default\":\"bottom-right\"},{\"name\":\"_clear\",\"kind\":\"Any\",\"default\":0},{\"name\":\"types\",\"kind\":\"Any\",\"default\":[{\"type\":\"map\",\"entries\":[[\"type\",\"warning\"],[\"background\",\"#ffc107\"],[\"icon\",{\"type\":\"map\",\"entries\":[[\"className\",\"fas fa-exclamation-triangle\"],[\"tagName\",\"i\"],[\"color\",\"white\"]]}]]},{\"type\":\"map\",\"entries\":[[\"type\",\"info\"],[\"background\",\"#007bff\"],[\"icon\",{\"type\":\"map\",\"entries\":[[\"className\",\"fas fa-info-circle\"],[\"tagName\",\"i\"],[\"color\",\"white\"]]}]]}]}]},{\"type\":\"model\",\"name\":\"Notification\",\"properties\":[{\"name\":\"background\",\"kind\":\"Any\",\"default\":null},{\"name\":\"duration\",\"kind\":\"Any\",\"default\":3000},{\"name\":\"icon\",\"kind\":\"Any\",\"default\":null},{\"name\":\"message\",\"kind\":\"Any\",\"default\":\"\"},{\"name\":\"notification_type\",\"kind\":\"Any\",\"default\":null},{\"name\":\"_destroyed\",\"kind\":\"Any\",\"default\":false}]},{\"type\":\"model\",\"name\":\"TemplateActions1\",\"properties\":[{\"name\":\"open_modal\",\"kind\":\"Any\",\"default\":0},{\"name\":\"close_modal\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"BootstrapTemplateActions1\",\"properties\":[{\"name\":\"open_modal\",\"kind\":\"Any\",\"default\":0},{\"name\":\"close_modal\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"MaterialTemplateActions1\",\"properties\":[{\"name\":\"open_modal\",\"kind\":\"Any\",\"default\":0},{\"name\":\"close_modal\",\"kind\":\"Any\",\"default\":0}]}],\"roots\":[{\"type\":\"object\",\"name\":\"panel.models.browser.BrowserInfo\",\"id\":\"8eee4fd3-3c98-4841-a231-c5a6bffcd827\"},{\"type\":\"object\",\"name\":\"panel.models.comm_manager.CommManager\",\"id\":\"536076e8-28dc-4f2e-87ec-f64a63b3a9d2\",\"attributes\":{\"plot_id\":\"8eee4fd3-3c98-4841-a231-c5a6bffcd827\",\"comm_id\":\"b69a941b35ee49cbb463e6da05fbb6d9\",\"client_comm_id\":\"bc871b4332f449ffbef8b44d9c1abd36\"}}],\"callbacks\":{\"type\":\"map\"}}};\n",
+       "  var render_items = [{\"docid\":\"891adebc-bb6a-44f8-be06-8f653d5b4253\",\"roots\":{\"8eee4fd3-3c98-4841-a231-c5a6bffcd827\":\"e557a804-4019-4291-9768-a9b903ba35af\"},\"root_ids\":[\"8eee4fd3-3c98-4841-a231-c5a6bffcd827\"]}];\n",
+       "  var docs = Object.values(docs_json)\n",
+       "  if (!docs) {\n",
+       "    return\n",
+       "  }\n",
+       "  const py_version = docs[0].version.replace('rc', '-rc.').replace('.dev', '-dev.')\n",
+       "  const is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1\n",
+       "  function embed_document(root) {\n",
+       "    var Bokeh = get_bokeh(root)\n",
+       "    Bokeh.embed.embed_items_notebook(docs_json, render_items);\n",
+       "    for (const render_item of render_items) {\n",
+       "      for (const root_id of render_item.root_ids) {\n",
+       "\tconst id_el = document.getElementById(root_id)\n",
+       "\tif (id_el.children.length && (id_el.children[0].className === 'bk-root')) {\n",
+       "\t  const root_el = id_el.children[0]\n",
+       "\t  root_el.id = root_el.id + '-rendered'\n",
+       "\t}\n",
+       "      }\n",
+       "    }\n",
+       "  }\n",
+       "  function get_bokeh(root) {\n",
+       "    if (root.Bokeh === undefined) {\n",
+       "      return null\n",
+       "    } else if (root.Bokeh.version !== py_version && !is_dev) {\n",
+       "      if (root.Bokeh.versions === undefined || !root.Bokeh.versions.has(py_version)) {\n",
+       "\treturn null\n",
+       "      }\n",
+       "      return root.Bokeh.versions.get(py_version);\n",
+       "    } else if (root.Bokeh.version === py_version) {\n",
+       "      return root.Bokeh\n",
+       "    }\n",
+       "    return null\n",
+       "  }\n",
+       "  function is_loaded(root) {\n",
+       "    var Bokeh = get_bokeh(root)\n",
+       "    return (Bokeh != null && Bokeh.Panel !== undefined && ( root['Tabulator'] !== undefined) && ( root['Tabulator'] !== undefined))\n",
+       "  }\n",
+       "  if (is_loaded(root)) {\n",
+       "    embed_document(root);\n",
+       "  } else {\n",
+       "    var attempts = 0;\n",
+       "    var timer = setInterval(function(root) {\n",
+       "      if (is_loaded(root)) {\n",
+       "        clearInterval(timer);\n",
+       "        embed_document(root);\n",
+       "      } else if (document.readyState == \"complete\") {\n",
+       "        attempts++;\n",
+       "        if (attempts > 200) {\n",
+       "          clearInterval(timer);\n",
+       "\t  var Bokeh = get_bokeh(root)\n",
+       "\t  if (Bokeh == null || Bokeh.Panel == null) {\n",
+       "            console.warn(\"Panel: ERROR: Unable to run Panel code because Bokeh or Panel library is missing\");\n",
+       "\t  } else {\n",
+       "\t    console.warn(\"Panel: WARNING: Attempting to render but not all required libraries could be resolved.\")\n",
+       "\t    embed_document(root)\n",
+       "\t  }\n",
+       "        }\n",
+       "      }\n",
+       "    }, 25, root)\n",
+       "  }\n",
+       "})(window);</script>"
+      ]
+     },
+     "metadata": {
+      "application/vnd.holoviews_exec.v0+json": {
+       "id": "8eee4fd3-3c98-4841-a231-c5a6bffcd827"
+      }
+     },
+     "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": [
+       "<style>*[data-root-id],\n",
+       "*[data-root-id] > * {\n",
+       "  box-sizing: border-box;\n",
+       "  font-family: var(--jp-ui-font-family);\n",
+       "  font-size: var(--jp-ui-font-size1);\n",
+       "  color: var(--vscode-editor-foreground, var(--jp-ui-font-color1));\n",
+       "}\n",
+       "\n",
+       "/* Override VSCode background color */\n",
+       ".cell-output-ipywidget-background:has(\n",
+       "    > .cell-output-ipywidget-background > .lm-Widget > *[data-root-id]\n",
+       "  ),\n",
+       ".cell-output-ipywidget-background:has(> .lm-Widget > *[data-root-id]) {\n",
+       "  background-color: transparent !important;\n",
+       "}\n",
+       "</style>"
+      ]
+     },
+     "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": [
+       "<style>*[data-root-id],\n",
+       "*[data-root-id] > * {\n",
+       "  box-sizing: border-box;\n",
+       "  font-family: var(--jp-ui-font-family);\n",
+       "  font-size: var(--jp-ui-font-size1);\n",
+       "  color: var(--vscode-editor-foreground, var(--jp-ui-font-color1));\n",
+       "}\n",
+       "\n",
+       "/* Override VSCode background color */\n",
+       ".cell-output-ipywidget-background:has(\n",
+       "    > .cell-output-ipywidget-background > .lm-Widget > *[data-root-id]\n",
+       "  ),\n",
+       ".cell-output-ipywidget-background:has(> .lm-Widget > *[data-root-id]) {\n",
+       "  background-color: transparent !important;\n",
+       "}\n",
+       "</style>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "import bdikit as bdi\n",
+    "import pandas as pd\n",
+    "\n",
+    "import flair, torch\n",
+    "flair.device = torch.device('cpu') "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Country</th>\n",
+       "      <th>Histologic_type</th>\n",
+       "      <th>FIGO_stage</th>\n",
+       "      <th>BMI</th>\n",
+       "      <th>Age</th>\n",
+       "      <th>Race</th>\n",
+       "      <th>Ethnicity</th>\n",
+       "      <th>Gender</th>\n",
+       "      <th>Tumor_Focality</th>\n",
+       "      <th>Tumor_Size_cm</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>38.88</td>\n",
+       "      <td>64.0</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>2.9</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>39.76</td>\n",
+       "      <td>58.0</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>3.5</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>51.19</td>\n",
+       "      <td>50.0</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>4.5</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>NaN</td>\n",
+       "      <td>Carcinosarcoma</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>32.69</td>\n",
+       "      <td>75.0</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>3.5</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>5</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>Serous</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>20.28</td>\n",
+       "      <td>63.0</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>6.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>6</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>55.67</td>\n",
+       "      <td>50.0</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>4.5</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>7</th>\n",
+       "      <td>Other_specify</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>25.68</td>\n",
+       "      <td>60.0</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>5.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>8</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>Serous</td>\n",
+       "      <td>IIIA</td>\n",
+       "      <td>21.57</td>\n",
+       "      <td>83.0</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>4.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>9</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>34.26</td>\n",
+       "      <td>69.0</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>5.2</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "         Country Histologic_type FIGO_stage    BMI   Age   Race  \\\n",
+       "0  United States    Endometrioid         IA  38.88  64.0  White   \n",
+       "1  United States    Endometrioid         IA  39.76  58.0  White   \n",
+       "2  United States    Endometrioid         IA  51.19  50.0  White   \n",
+       "3            NaN  Carcinosarcoma        NaN    NaN   NaN    NaN   \n",
+       "4  United States    Endometrioid         IA  32.69  75.0  White   \n",
+       "5  United States          Serous         IA  20.28  63.0  White   \n",
+       "6  United States    Endometrioid         IA  55.67  50.0  White   \n",
+       "7  Other_specify    Endometrioid         IA  25.68  60.0  White   \n",
+       "8  United States          Serous       IIIA  21.57  83.0  White   \n",
+       "9  United States    Endometrioid         IA  34.26  69.0  White   \n",
+       "\n",
+       "                Ethnicity  Gender Tumor_Focality  Tumor_Size_cm  \n",
+       "0  Not-Hispanic or Latino  Female       Unifocal            2.9  \n",
+       "1  Not-Hispanic or Latino  Female       Unifocal            3.5  \n",
+       "2  Not-Hispanic or Latino  Female       Unifocal            4.5  \n",
+       "3                     NaN     NaN            NaN            NaN  \n",
+       "4  Not-Hispanic or Latino  Female       Unifocal            3.5  \n",
+       "5  Not-Hispanic or Latino  Female       Unifocal            6.0  \n",
+       "6  Not-Hispanic or Latino  Female       Unifocal            4.5  \n",
+       "7  Not-Hispanic or Latino  Female       Unifocal            5.0  \n",
+       "8  Not-Hispanic or Latino  Female       Unifocal            4.0  \n",
+       "9  Not-Hispanic or Latino  Female       Unifocal            5.2  "
+      ]
+     },
+     "execution_count": 2,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "dataset = pd.read_csv('./datasets/dou.csv')\n",
+    "\n",
+    "columns = [\n",
+    "    \"Country\",\n",
+    "    \"Histologic_type\",\n",
+    "    \"FIGO_stage\",\n",
+    "    \"BMI\",\n",
+    "    \"Age\",\n",
+    "    \"Race\",\n",
+    "    \"Ethnicity\",\n",
+    "    \"Gender\",\n",
+    "    \"Tumor_Focality\",\n",
+    "    \"Tumor_Size_cm\",\n",
+    "]\n",
+    "\n",
+    "dataset[columns].head(10)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Matching the table schema to GDC standard vocabulary\n",
+    "\n",
+    "`bdi-kit` offers a suite of functions to help with data harmonization task.\n",
+    "For instance, it can help with automatic discovery of one-to-one mappings between the columns in the input (source) dataset and a target dataset schema. The target schema can be either another table or standard data vocabulary such as the GDC (Genomic Data Commons).\n",
+    "\n",
+    "To achive this using `bdi-kit`, we can use the `match_columns()` function to the GDC vocabulary as follows."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "  0%|          | 0/10 [00:00<?, ?it/s]We strongly recommend passing in an `attention_mask` since your input_ids may be padded. See https://huggingface.co/docs/transformers/troubleshooting#incorrect-output-when-padding-tokens-arent-masked.\n",
+      "100%|██████████| 10/10 [00:01<00:00,  9.84it/s]\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Table features extracted from 10 columns\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "100%|██████████| 734/734 [01:03<00:00, 11.51it/s]\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Table features extracted from 734 columns\n"
+     ]
+    },
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Country</td>\n",
+       "      <td>country_of_birth</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Histologic_type</td>\n",
+       "      <td>dysplasia_type</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>FIGO_stage</td>\n",
+       "      <td>figo_stage</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>BMI</td>\n",
+       "      <td>hpv_positive_type</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>Age</td>\n",
+       "      <td>weight</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>5</th>\n",
+       "      <td>Race</td>\n",
+       "      <td>race</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>6</th>\n",
+       "      <td>Ethnicity</td>\n",
+       "      <td>ethnicity</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>7</th>\n",
+       "      <td>Gender</td>\n",
+       "      <td>gender</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>8</th>\n",
+       "      <td>Tumor_Focality</td>\n",
+       "      <td>tumor_focality</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>9</th>\n",
+       "      <td>Tumor_Size_cm</td>\n",
+       "      <td>tumor_depth</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "            source             target\n",
+       "0          Country   country_of_birth\n",
+       "1  Histologic_type     dysplasia_type\n",
+       "2       FIGO_stage         figo_stage\n",
+       "3              BMI  hpv_positive_type\n",
+       "4              Age             weight\n",
+       "5             Race               race\n",
+       "6        Ethnicity          ethnicity\n",
+       "7           Gender             gender\n",
+       "8   Tumor_Focality     tumor_focality\n",
+       "9    Tumor_Size_cm        tumor_depth"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "column_mappings = bdi.match_columns(dataset[columns], target='gdc', method='two_phase')\n",
+    "column_mappings"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Generating a harmonized table\n",
+    "\n",
+    "After discovering a schema mapping, we can generate a new table (DataFrame) using the new column names from the GDC starndard vocabulary.\n",
+    "\n",
+    "To do so using `bdi-kit`, we can use the function `materialize_mapping()` as follows. Note that the column headers have been renamed to the target schema."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>country_of_birth</th>\n",
+       "      <th>dysplasia_type</th>\n",
+       "      <th>figo_stage</th>\n",
+       "      <th>hpv_positive_type</th>\n",
+       "      <th>weight</th>\n",
+       "      <th>race</th>\n",
+       "      <th>ethnicity</th>\n",
+       "      <th>gender</th>\n",
+       "      <th>tumor_focality</th>\n",
+       "      <th>tumor_depth</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>38.88</td>\n",
+       "      <td>64.0</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>2.9</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>39.76</td>\n",
+       "      <td>58.0</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>3.5</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>51.19</td>\n",
+       "      <td>50.0</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>4.5</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>NaN</td>\n",
+       "      <td>Carcinosarcoma</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>32.69</td>\n",
+       "      <td>75.0</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>3.5</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>...</th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>99</th>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>29.40</td>\n",
+       "      <td>75.0</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>4.2</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>100</th>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>II</td>\n",
+       "      <td>35.42</td>\n",
+       "      <td>74.0</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>1.5</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>101</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>Serous</td>\n",
+       "      <td>II</td>\n",
+       "      <td>24.32</td>\n",
+       "      <td>85.0</td>\n",
+       "      <td>Black or African American</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>3.8</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>102</th>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>Serous</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>34.06</td>\n",
+       "      <td>70.0</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>5.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>103</th>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>Serous</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "<p>104 rows × 10 columns</p>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "    country_of_birth  dysplasia_type figo_stage  hpv_positive_type  weight  \\\n",
+       "0      United States    Endometrioid         IA              38.88    64.0   \n",
+       "1      United States    Endometrioid         IA              39.76    58.0   \n",
+       "2      United States    Endometrioid         IA              51.19    50.0   \n",
+       "3                NaN  Carcinosarcoma        NaN                NaN     NaN   \n",
+       "4      United States    Endometrioid         IA              32.69    75.0   \n",
+       "..               ...             ...        ...                ...     ...   \n",
+       "99           Ukraine    Endometrioid         IA              29.40    75.0   \n",
+       "100          Ukraine    Endometrioid         II              35.42    74.0   \n",
+       "101    United States          Serous         II              24.32    85.0   \n",
+       "102          Ukraine          Serous         IA              34.06    70.0   \n",
+       "103          Ukraine          Serous        NaN                NaN     NaN   \n",
+       "\n",
+       "                          race               ethnicity  gender tumor_focality  \\\n",
+       "0                        White  Not-Hispanic or Latino  Female       Unifocal   \n",
+       "1                        White  Not-Hispanic or Latino  Female       Unifocal   \n",
+       "2                        White  Not-Hispanic or Latino  Female       Unifocal   \n",
+       "3                          NaN                     NaN     NaN            NaN   \n",
+       "4                        White  Not-Hispanic or Latino  Female       Unifocal   \n",
+       "..                         ...                     ...     ...            ...   \n",
+       "99                         NaN                     NaN  Female       Unifocal   \n",
+       "100                        NaN                     NaN  Female       Unifocal   \n",
+       "101  Black or African American  Not-Hispanic or Latino  Female       Unifocal   \n",
+       "102                        NaN                     NaN  Female       Unifocal   \n",
+       "103                        NaN                     NaN     NaN            NaN   \n",
+       "\n",
+       "     tumor_depth  \n",
+       "0            2.9  \n",
+       "1            3.5  \n",
+       "2            4.5  \n",
+       "3            NaN  \n",
+       "4            3.5  \n",
+       "..           ...  \n",
+       "99           4.2  \n",
+       "100          1.5  \n",
+       "101          3.8  \n",
+       "102          5.0  \n",
+       "103          NaN  \n",
+       "\n",
+       "[104 rows x 10 columns]"
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "bdi.materialize_mapping(dataset, column_mappings)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Generating a harmonized table with value mappings\n",
+    "\n",
+    "`bdi-kit` can also help with translation of the values from source table to the target starndard format.\n",
+    "\n",
+    "To this end, `bdi-kit` provides the function `match_values()` that automatically creates value mappings for each string column.\n",
+    "The output of `match_values()` can be fed to `materialize_mapping()` which materialized the final target using both schema and value mappings."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>country_of_birth</th>\n",
+       "      <th>dysplasia_type</th>\n",
+       "      <th>figo_stage</th>\n",
+       "      <th>race</th>\n",
+       "      <th>ethnicity</th>\n",
+       "      <th>gender</th>\n",
+       "      <th>tumor_focality</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>None</td>\n",
+       "      <td>Stage IA</td>\n",
+       "      <td>white</td>\n",
+       "      <td>not hispanic or latino</td>\n",
+       "      <td>female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>None</td>\n",
+       "      <td>Stage IA</td>\n",
+       "      <td>white</td>\n",
+       "      <td>not hispanic or latino</td>\n",
+       "      <td>female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>None</td>\n",
+       "      <td>Stage IA</td>\n",
+       "      <td>white</td>\n",
+       "      <td>not hispanic or latino</td>\n",
+       "      <td>female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>NaN</td>\n",
+       "      <td>Esophageal Mucosa Columnar Dysplasia</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>None</td>\n",
+       "      <td>Stage IA</td>\n",
+       "      <td>white</td>\n",
+       "      <td>not hispanic or latino</td>\n",
+       "      <td>female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>...</th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>99</th>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>None</td>\n",
+       "      <td>Stage IA</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>100</th>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>None</td>\n",
+       "      <td>Stage III</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>101</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>None</td>\n",
+       "      <td>Stage III</td>\n",
+       "      <td>black or african american</td>\n",
+       "      <td>not hispanic or latino</td>\n",
+       "      <td>female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>102</th>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>None</td>\n",
+       "      <td>Stage IA</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>103</th>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>None</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "<p>104 rows × 7 columns</p>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "    country_of_birth                        dysplasia_type figo_stage  \\\n",
+       "0      United States                                  None   Stage IA   \n",
+       "1      United States                                  None   Stage IA   \n",
+       "2      United States                                  None   Stage IA   \n",
+       "3                NaN  Esophageal Mucosa Columnar Dysplasia        NaN   \n",
+       "4      United States                                  None   Stage IA   \n",
+       "..               ...                                   ...        ...   \n",
+       "99           Ukraine                                  None   Stage IA   \n",
+       "100          Ukraine                                  None  Stage III   \n",
+       "101    United States                                  None  Stage III   \n",
+       "102          Ukraine                                  None   Stage IA   \n",
+       "103          Ukraine                                  None        NaN   \n",
+       "\n",
+       "                          race               ethnicity  gender tumor_focality  \n",
+       "0                        white  not hispanic or latino  female       Unifocal  \n",
+       "1                        white  not hispanic or latino  female       Unifocal  \n",
+       "2                        white  not hispanic or latino  female       Unifocal  \n",
+       "3                          NaN                     NaN     NaN            NaN  \n",
+       "4                        white  not hispanic or latino  female       Unifocal  \n",
+       "..                         ...                     ...     ...            ...  \n",
+       "99                         NaN                     NaN  female       Unifocal  \n",
+       "100                        NaN                     NaN  female       Unifocal  \n",
+       "101  black or african american  not hispanic or latino  female       Unifocal  \n",
+       "102                        NaN                     NaN  female       Unifocal  \n",
+       "103                        NaN                     NaN     NaN            NaN  \n",
+       "\n",
+       "[104 rows x 7 columns]"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "value_mappings = bdi.match_values(dataset, column_mapping=column_mappings, target='gdc', method='tfidf')\n",
+    "bdi.materialize_mapping(dataset, value_mappings)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Verifying the schema mappings\n",
+    "\n",
+    "Sometimes the mappings generated automatically may be incorrect or you may want verify them individualy.\n",
+    "To verify the suggested column mappings, `bdi-kit` offers additional APIs to visualizing the data and make any modification when necessary. \n",
+    "\n",
+    "For this example, we will use the column `Histologic_type`. We can start by exploring are the columns most similar to `Histologic_type`. \n",
+    "\n",
+    "For this we can use the `top_matches()` function. Here, we notice that `primary_diagnosis` could be potential target column.\n"
+   ]
+  },
+  {
+   "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, 14.70it/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:58<00:00, 12.48it/s]"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Table features extracted from 734 columns\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "\n"
+     ]
+    },
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "      <th>similarity</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Histologic_type</td>\n",
+       "      <td>described_cases</td>\n",
+       "      <td>0.589956</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Histologic_type</td>\n",
+       "      <td>slide_images</td>\n",
+       "      <td>0.587552</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>Histologic_type</td>\n",
+       "      <td>history_of_tumor_type</td>\n",
+       "      <td>0.574640</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>Histologic_type</td>\n",
+       "      <td>primary_diagnosis</td>\n",
+       "      <td>0.573583</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>Histologic_type</td>\n",
+       "      <td>additional_pathology_findings</td>\n",
+       "      <td>0.562278</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>5</th>\n",
+       "      <td>Histologic_type</td>\n",
+       "      <td>pathology_details</td>\n",
+       "      <td>0.562007</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>6</th>\n",
+       "      <td>Histologic_type</td>\n",
+       "      <td>pathology_reports</td>\n",
+       "      <td>0.547307</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>7</th>\n",
+       "      <td>Histologic_type</td>\n",
+       "      <td>relationship_primary_diagnosis</td>\n",
+       "      <td>0.524285</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>8</th>\n",
+       "      <td>Histologic_type</td>\n",
+       "      <td>diagnoses</td>\n",
+       "      <td>0.519854</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>9</th>\n",
+       "      <td>Histologic_type</td>\n",
+       "      <td>family_histories</td>\n",
+       "      <td>0.516649</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "            source                          target  similarity\n",
+       "0  Histologic_type                 described_cases    0.589956\n",
+       "1  Histologic_type                    slide_images    0.587552\n",
+       "2  Histologic_type           history_of_tumor_type    0.574640\n",
+       "3  Histologic_type               primary_diagnosis    0.573583\n",
+       "4  Histologic_type   additional_pathology_findings    0.562278\n",
+       "5  Histologic_type               pathology_details    0.562007\n",
+       "6  Histologic_type               pathology_reports    0.547307\n",
+       "7  Histologic_type  relationship_primary_diagnosis    0.524285\n",
+       "8  Histologic_type                       diagnoses    0.519854\n",
+       "9  Histologic_type                family_histories    0.516649"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "hist_type_matches = bdi.top_matches(dataset, columns=['Histologic_type'], target='gdc')\n",
+    "hist_type_matches"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "##### Viewing the column domains\n",
+    "\n",
+    "To verify that `primary_diagnosis` is a good target column, we view and compare the domains of each column using the `preview_domain()` function. For the source table, it returns the list of unique values in the source column. For the GDC target, it returns the list of unique valid values that a column can have.\n",
+    "\n",
+    "Here we see that the values seem to be related."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source_domain</th>\n",
+       "      <th>target_domain</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>Abdominal desmoid</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Carcinosarcoma</td>\n",
+       "      <td>Abdominal fibromatosis</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>Serous</td>\n",
+       "      <td>Achromic nevus</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>Clear cell</td>\n",
+       "      <td>Acidophil adenocarcinoma</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td></td>\n",
+       "      <td>Acidophil adenoma</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>...</th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2620</th>\n",
+       "      <td></td>\n",
+       "      <td>Wolffian duct tumor</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2621</th>\n",
+       "      <td></td>\n",
+       "      <td>Xanthofibroma</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2622</th>\n",
+       "      <td></td>\n",
+       "      <td>Yolk sac tumor</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2623</th>\n",
+       "      <td></td>\n",
+       "      <td>Unknown</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2624</th>\n",
+       "      <td></td>\n",
+       "      <td>Not Reported</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "<p>2625 rows × 2 columns</p>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "       source_domain             target_domain\n",
+       "0       Endometrioid         Abdominal desmoid\n",
+       "1     Carcinosarcoma    Abdominal fibromatosis\n",
+       "2             Serous            Achromic nevus\n",
+       "3         Clear cell  Acidophil adenocarcinoma\n",
+       "4                            Acidophil adenoma\n",
+       "...              ...                       ...\n",
+       "2620                       Wolffian duct tumor\n",
+       "2621                             Xanthofibroma\n",
+       "2622                            Yolk sac tumor\n",
+       "2623                                   Unknown\n",
+       "2624                              Not Reported\n",
+       "\n",
+       "[2625 rows x 2 columns]"
+      ]
+     },
+     "execution_count": 7,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "bdi.preview_domains(\n",
+    "    dataset, column_mapping=(\"Histologic_type\", \"primary_diagnosis\"), target=\"gdc\"\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Since `primary_diagnosis` looks like a correct match for `Histologic_type`, we can modify the `column_mappings` variable directly."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Country</td>\n",
+       "      <td>country_of_birth</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Histologic_type</td>\n",
+       "      <td>primary_diagnosis</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>FIGO_stage</td>\n",
+       "      <td>figo_stage</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>BMI</td>\n",
+       "      <td>hpv_positive_type</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>Age</td>\n",
+       "      <td>weight</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>5</th>\n",
+       "      <td>Race</td>\n",
+       "      <td>race</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>6</th>\n",
+       "      <td>Ethnicity</td>\n",
+       "      <td>ethnicity</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>7</th>\n",
+       "      <td>Gender</td>\n",
+       "      <td>gender</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>8</th>\n",
+       "      <td>Tumor_Focality</td>\n",
+       "      <td>tumor_focality</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>9</th>\n",
+       "      <td>Tumor_Size_cm</td>\n",
+       "      <td>tumor_depth</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "            source             target\n",
+       "0          Country   country_of_birth\n",
+       "1  Histologic_type  primary_diagnosis\n",
+       "2       FIGO_stage         figo_stage\n",
+       "3              BMI  hpv_positive_type\n",
+       "4              Age             weight\n",
+       "5             Race               race\n",
+       "6        Ethnicity          ethnicity\n",
+       "7           Gender             gender\n",
+       "8   Tumor_Focality     tumor_focality\n",
+       "9    Tumor_Size_cm        tumor_depth"
+      ]
+     },
+     "execution_count": 8,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "column_mappings.loc[column_mappings['source'] == 'Histologic_type', 'target'] = 'primary_diagnosis'\n",
+    "column_mappings\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "##### Finding correct value mappings\n",
+    "\n",
+    "After finding the correct column, we need to find appropriate value mappings. \n",
+    "Using `preview_value_mappings()`, we can inspect what are the possible value mappings for this would look like after the harmonization.\n",
+    "\n",
+    "`bdi-kit` implements multiple methods for value mapping discovery, inncluding:\n",
+    "- `edit_distance` - Computes value similarities using Levenstein's edit distance measure.\n",
+    "- `tfidf` - A method based on tf-idf importance weighting computed over charcter n-grams.\n",
+    "- `embeddings` - Uses BERT word embeddings to compute \"semantic similarity\" between the value.\n",
+    "\n",
+    "To specify a value mapping approach, we can pass the `method` parameter."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "      <th>similarity</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Carcinosarcoma</td>\n",
+       "      <td>Carcinosarcoma, NOS</td>\n",
+       "      <td>0.848485</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Clear cell</td>\n",
+       "      <td>Clear cell adenoma</td>\n",
+       "      <td>0.714286</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>Stromal endometriosis</td>\n",
+       "      <td>0.666667</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>Serous</td>\n",
+       "      <td>Neuronevus</td>\n",
+       "      <td>0.625000</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "           source                 target  similarity\n",
+       "0  Carcinosarcoma    Carcinosarcoma, NOS    0.848485\n",
+       "1      Clear cell     Clear cell adenoma    0.714286\n",
+       "2    Endometrioid  Stromal endometriosis    0.666667\n",
+       "3          Serous             Neuronevus    0.625000"
+      ]
+     },
+     "execution_count": 9,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "bdi.preview_value_mappings(\n",
+    "    dataset, column_mapping=(\"Histologic_type\", \"primary_diagnosis\"), target=\"gdc\", method=\"edit_distance\"\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "      <th>similarity</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Carcinosarcoma</td>\n",
+       "      <td>Carcinosarcoma, NOS</td>\n",
+       "      <td>0.969</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>Endometrioid adenoma, NOS</td>\n",
+       "      <td>0.897</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>Clear cell</td>\n",
+       "      <td>Clear cell adenoma</td>\n",
+       "      <td>0.853</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>Serous</td>\n",
+       "      <td>Serous carcinoma, NOS</td>\n",
+       "      <td>0.755</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "           source                     target  similarity\n",
+       "0  Carcinosarcoma        Carcinosarcoma, NOS       0.969\n",
+       "1    Endometrioid  Endometrioid adenoma, NOS       0.897\n",
+       "2      Clear cell         Clear cell adenoma       0.853\n",
+       "3          Serous      Serous carcinoma, NOS       0.755"
+      ]
+     },
+     "execution_count": 10,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "bdi.preview_value_mappings(\n",
+    "    dataset, column_mapping=(\"Histologic_type\", \"primary_diagnosis\"), target=\"gdc\", method=\"tfidf\"\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "      <th>similarity</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Carcinosarcoma</td>\n",
+       "      <td>Carcinofibroma</td>\n",
+       "      <td>0.919</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>Endometrioid cystadenocarcinoma</td>\n",
+       "      <td>0.810</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>Clear cell</td>\n",
+       "      <td>Clear cell carcinoma</td>\n",
+       "      <td>0.760</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>Serous</td>\n",
+       "      <td>Serous cystoma</td>\n",
+       "      <td>0.661</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "           source                           target  similarity\n",
+       "0  Carcinosarcoma                   Carcinofibroma       0.919\n",
+       "1    Endometrioid  Endometrioid cystadenocarcinoma       0.810\n",
+       "2      Clear cell             Clear cell carcinoma       0.760\n",
+       "3          Serous                   Serous cystoma       0.661"
+      ]
+     },
+     "execution_count": 11,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "bdi.preview_value_mappings(\n",
+    "    dataset, column_mapping=(\"Histologic_type\", \"primary_diagnosis\"), target=\"gdc\", method=\"embedding\"\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Carcinosarcoma</td>\n",
+       "      <td>Carcinosarcoma, NOS</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Clear cell</td>\n",
+       "      <td>Clear cell adenocarcinoma, NOS</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>Endometrioid carcinoma</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>Serous</td>\n",
+       "      <td>Serous cystadenocarcinoma</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "           source                          target\n",
+       "0  Carcinosarcoma             Carcinosarcoma, NOS\n",
+       "1      Clear cell  Clear cell adenocarcinoma, NOS\n",
+       "2    Endometrioid          Endometrioid carcinoma\n",
+       "3          Serous       Serous cystadenocarcinoma"
+      ]
+     },
+     "execution_count": 12,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "hist_type_vmap = pd.DataFrame(\n",
+    "    columns=[\"source\", \"target\"],\n",
+    "    data=[\n",
+    "        (\"Carcinosarcoma\", \"Carcinosarcoma, NOS\"),\n",
+    "        (\"Clear cell\", \"Clear cell adenocarcinoma, NOS\"),\n",
+    "        (\"Endometrioid\", \"Endometrioid carcinoma\"),\n",
+    "        (\"Serous\", \"Serous cystadenocarcinoma\"),\n",
+    "    ],\n",
+    ")\n",
+    "hist_type_vmap"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Verifying multiple value mappings at once\n",
+    "\n",
+    "Besides verifying value mappings individually, you can also to it for all column mappings at once."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Country => country_of_birth\n"
+     ]
+    },
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "      <th>similarity</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>United States</td>\n",
+       "      <td>United States</td>\n",
+       "      <td>1.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>1.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>Poland</td>\n",
+       "      <td>Poland</td>\n",
+       "      <td>1.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>nan</td>\n",
+       "      <td>None</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>Other_specify</td>\n",
+       "      <td>None</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "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           None         NaN\n",
+       "4  Other_specify           None         NaN"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "Histologic_type => primary_diagnosis\n"
+     ]
+    },
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "      <th>similarity</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Carcinosarcoma</td>\n",
+       "      <td>Carcinosarcoma, NOS</td>\n",
+       "      <td>0.969</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>Endometrioid adenoma, NOS</td>\n",
+       "      <td>0.897</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>Clear cell</td>\n",
+       "      <td>Clear cell adenoma</td>\n",
+       "      <td>0.853</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>Serous</td>\n",
+       "      <td>Serous carcinoma, NOS</td>\n",
+       "      <td>0.755</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "           source                     target  similarity\n",
+       "0  Carcinosarcoma        Carcinosarcoma, NOS       0.969\n",
+       "1    Endometrioid  Endometrioid adenoma, NOS       0.897\n",
+       "2      Clear cell         Clear cell adenoma       0.853\n",
+       "3          Serous      Serous carcinoma, NOS       0.755"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "FIGO_stage => figo_stage\n"
+     ]
+    },
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "      <th>similarity</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>IIIC2</td>\n",
+       "      <td>Stage IIIC2</td>\n",
+       "      <td>0.889</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>IIIC1</td>\n",
+       "      <td>Stage IIIC1</td>\n",
+       "      <td>0.889</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>IVB</td>\n",
+       "      <td>Stage IVB</td>\n",
+       "      <td>0.854</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>IIIB</td>\n",
+       "      <td>Stage IIIB</td>\n",
+       "      <td>0.849</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>IIIA</td>\n",
+       "      <td>Stage IIIA</td>\n",
+       "      <td>0.822</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>5</th>\n",
+       "      <td>II</td>\n",
+       "      <td>Stage III</td>\n",
+       "      <td>0.687</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>6</th>\n",
+       "      <td>IB</td>\n",
+       "      <td>Stage IB</td>\n",
+       "      <td>0.649</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>7</th>\n",
+       "      <td>IA</td>\n",
+       "      <td>Stage IA</td>\n",
+       "      <td>0.586</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>8</th>\n",
+       "      <td>nan</td>\n",
+       "      <td>Unknown</td>\n",
+       "      <td>0.350</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "  source       target  similarity\n",
+       "0  IIIC2  Stage IIIC2       0.889\n",
+       "1  IIIC1  Stage IIIC1       0.889\n",
+       "2    IVB    Stage IVB       0.854\n",
+       "3   IIIB   Stage IIIB       0.849\n",
+       "4   IIIA   Stage IIIA       0.822\n",
+       "5     II    Stage III       0.687\n",
+       "6     IB     Stage IB       0.649\n",
+       "7     IA     Stage IA       0.586\n",
+       "8    nan      Unknown       0.350"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "Race => race\n"
+     ]
+    },
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "      <th>similarity</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>White</td>\n",
+       "      <td>white</td>\n",
+       "      <td>1.000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Asian</td>\n",
+       "      <td>asian</td>\n",
+       "      <td>1.000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>Not Reported</td>\n",
+       "      <td>not reported</td>\n",
+       "      <td>1.000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>Black or African American</td>\n",
+       "      <td>black or african american</td>\n",
+       "      <td>1.000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>nan</td>\n",
+       "      <td>american indian or alaska native</td>\n",
+       "      <td>0.359</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "                      source                            target  similarity\n",
+       "0                      White                             white       1.000\n",
+       "1                      Asian                             asian       1.000\n",
+       "2               Not Reported                      not reported       1.000\n",
+       "3  Black or African American         black or african american       1.000\n",
+       "4                        nan  american indian or alaska native       0.359"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "Ethnicity => ethnicity\n"
+     ]
+    },
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "      <th>similarity</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Hispanic or Latino</td>\n",
+       "      <td>hispanic or latino</td>\n",
+       "      <td>1.000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>not hispanic or latino</td>\n",
+       "      <td>0.935</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>Not reported</td>\n",
+       "      <td>not hispanic or latino</td>\n",
+       "      <td>0.268</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>nan</td>\n",
+       "      <td>None</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "                   source                  target  similarity\n",
+       "0      Hispanic or Latino      hispanic or latino       1.000\n",
+       "1  Not-Hispanic or Latino  not hispanic or latino       0.935\n",
+       "2            Not reported  not hispanic or latino       0.268\n",
+       "3                     nan                    None         NaN"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "Gender => gender\n"
+     ]
+    },
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "      <th>similarity</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Female</td>\n",
+       "      <td>female</td>\n",
+       "      <td>1.00</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>nan</td>\n",
+       "      <td>unknown</td>\n",
+       "      <td>0.29</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "   source   target  similarity\n",
+       "0  Female   female        1.00\n",
+       "1     nan  unknown        0.29"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "Tumor_Focality => tumor_focality\n"
+     ]
+    },
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "      <th>similarity</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>Unifocal</td>\n",
+       "      <td>1.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Multifocal</td>\n",
+       "      <td>Multifocal</td>\n",
+       "      <td>1.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>nan</td>\n",
+       "      <td>None</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "       source      target  similarity\n",
+       "0    Unifocal    Unifocal         1.0\n",
+       "1  Multifocal  Multifocal         1.0\n",
+       "2         nan        None         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=\"tfidf\",\n",
+    ")\n",
+    "\n",
+    "for mapping in mappings:\n",
+    "    print(f\"{mapping['source']} => {mapping['target']}\")\n",
+    "    display(mapping[\"mapping\"])\n",
+    "    print(\"\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Fixing remaining value mappings\n",
+    "\n",
+    "We need fix a few value mappings:\n",
+    "- Race\n",
+    "- Ethnicity\n",
+    "- Tumor_Site\n",
+    "\n",
+    "For race, we need to fix `nan` -> `merican indian or alaska native`."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "      <th>similarity</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>White</td>\n",
+       "      <td>white</td>\n",
+       "      <td>1.000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Asian</td>\n",
+       "      <td>asian</td>\n",
+       "      <td>1.000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>Not Reported</td>\n",
+       "      <td>not reported</td>\n",
+       "      <td>1.000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>Black or African American</td>\n",
+       "      <td>black or african american</td>\n",
+       "      <td>1.000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>nan</td>\n",
+       "      <td>american indian or alaska native</td>\n",
+       "      <td>0.359</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "                      source                            target  similarity\n",
+       "0                      White                             white       1.000\n",
+       "1                      Asian                             asian       1.000\n",
+       "2               Not Reported                      not reported       1.000\n",
+       "3  Black or African American         black or african american       1.000\n",
+       "4                        nan  american indian or alaska native       0.359"
+      ]
+     },
+     "execution_count": 14,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "race_vmap = bdi.preview_value_mappings(\n",
+    "    dataset,\n",
+    "    column_mapping=(\"Race\", \"race\"),\n",
+    "    target=\"gdc\",\n",
+    "    method=\"tfidf\",\n",
+    ")\n",
+    "race_vmap"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 15,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "      <th>similarity</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>White</td>\n",
+       "      <td>white</td>\n",
+       "      <td>1.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Asian</td>\n",
+       "      <td>asian</td>\n",
+       "      <td>1.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>Not Reported</td>\n",
+       "      <td>not reported</td>\n",
+       "      <td>1.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>Black or African American</td>\n",
+       "      <td>black or african american</td>\n",
+       "      <td>1.0</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "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"
+      ]
+     },
+     "execution_count": 15,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "race_vmap = race_vmap[race_vmap['similarity'] >= 1.0]\n",
+    "race_vmap"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "For `Ethnicity` we nee dot fix `Not reported` -> `not hispanic or latino`."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "      <th>similarity</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Hispanic or Latino</td>\n",
+       "      <td>hispanic or latino</td>\n",
+       "      <td>1.000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>not hispanic or latino</td>\n",
+       "      <td>0.935</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>Not reported</td>\n",
+       "      <td>not hispanic or latino</td>\n",
+       "      <td>0.268</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>nan</td>\n",
+       "      <td>None</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "                   source                  target  similarity\n",
+       "0      Hispanic or Latino      hispanic or latino       1.000\n",
+       "1  Not-Hispanic or Latino  not hispanic or latino       0.935\n",
+       "2            Not reported  not hispanic or latino       0.268\n",
+       "3                     nan                    None         NaN"
+      ]
+     },
+     "execution_count": 16,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "ethinicity_vmap = bdi.preview_value_mappings(\n",
+    "    dataset,\n",
+    "    column_mapping=(\"Ethnicity\", \"ethnicity\"),\n",
+    "    target=\"gdc\",\n",
+    "    method=\"tfidf\",\n",
+    ")\n",
+    "ethinicity_vmap\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 17,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "      <th>similarity</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Hispanic or Latino</td>\n",
+       "      <td>hispanic or latino</td>\n",
+       "      <td>1.000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>not hispanic or latino</td>\n",
+       "      <td>0.935</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "                   source                  target  similarity\n",
+       "0      Hispanic or Latino      hispanic or latino       1.000\n",
+       "1  Not-Hispanic or Latino  not hispanic or latino       0.935"
+      ]
+     },
+     "execution_count": 17,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "ethinicity_vmap = ethinicity_vmap[ethinicity_vmap[\"similarity\"] > 0.9]\n",
+    "ethinicity_vmap"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "For `Tumor_Site`, given that this dataset is about endometrial cancer, all values must be mapped to \"Endometrium\"."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 18,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source</th>\n",
+       "      <th>target</th>\n",
+       "      <th>similarity</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Anterior endometrium</td>\n",
+       "      <td>Endometrium</td>\n",
+       "      <td>0.852</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Posterior endometrium</td>\n",
+       "      <td>Endometrium</td>\n",
+       "      <td>0.823</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>Other, specify</td>\n",
+       "      <td>Other specified parts of pancreas</td>\n",
+       "      <td>0.543</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>nan</td>\n",
+       "      <td>Anal canal</td>\n",
+       "      <td>0.301</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "                  source                             target  similarity\n",
+       "0   Anterior endometrium                        Endometrium       0.852\n",
+       "1  Posterior endometrium                        Endometrium       0.823\n",
+       "2         Other, specify  Other specified parts of pancreas       0.543\n",
+       "3                    nan                         Anal canal       0.301"
+      ]
+     },
+     "execution_count": 18,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "bdi.preview_value_mappings(\n",
+    "    dataset, column_mapping=(\"Tumor_Site\", \"tissue_or_organ_of_origin\"), target=\"gdc\", method=\"tfidf\"\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 19,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Custom mapping function that will be used to map the values of the 'Tumor_Site' column\n",
+    "def map_tumor_site(source_value):\n",
+    "    return \"Endometrium\""
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Combining custom user mappings with suggested mappings"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 20,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from math import ceil\n",
+    "\n",
+    "user_mappings = [\n",
+    "    {\n",
+    "        \"source\": \"Tumor_Site\",\n",
+    "        \"target\": \"tissue_or_organ_of_origin\",\n",
+    "        \"mapper\": map_tumor_site,\n",
+    "    },\n",
+    "    {\n",
+    "        \"source\": \"BMI\",\n",
+    "        \"target\": \"bmi\",\n",
+    "    },\n",
+    "    {\n",
+    "        \"source\": \"Age\",\n",
+    "        \"target\": \"days_to_birth\",\n",
+    "        \"mapper\": lambda age: -age * 365.25,\n",
+    "    },\n",
+    "    {\n",
+    "        \"source\": \"Age\",\n",
+    "        \"target\": \"age_at_diagnosis\",\n",
+    "        \"mapper\": lambda age: float(\"nan\") if pd.isnull(age) else ceil(age*365.25),\n",
+    "    },\n",
+    "    {\n",
+    "        \"source\": \"Tumor_Size_cm\",\n",
+    "        \"target\": \"tumor_largest_dimension_diameter\",\n",
+    "    }\n",
+    "]\n",
+    "\n",
+    "value_mappings = bdi.match_values(\n",
+    "    dataset, target=\"gdc\", column_mapping=column_mappings, method=\"tfidf\"\n",
+    ")\n",
+    "\n",
+    "harmonization_spec = bdi.update_mappings(value_mappings, user_mappings)\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 21,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>tissue_or_organ_of_origin</th>\n",
+       "      <th>bmi</th>\n",
+       "      <th>days_to_birth</th>\n",
+       "      <th>age_at_diagnosis</th>\n",
+       "      <th>tumor_largest_dimension_diameter</th>\n",
+       "      <th>country_of_birth</th>\n",
+       "      <th>primary_diagnosis</th>\n",
+       "      <th>figo_stage</th>\n",
+       "      <th>race</th>\n",
+       "      <th>ethnicity</th>\n",
+       "      <th>gender</th>\n",
+       "      <th>tumor_focality</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Endometrium</td>\n",
+       "      <td>38.88</td>\n",
+       "      <td>-23376.00</td>\n",
+       "      <td>23376.0</td>\n",
+       "      <td>2.9</td>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid adenoma, NOS</td>\n",
+       "      <td>Stage IA</td>\n",
+       "      <td>white</td>\n",
+       "      <td>not hispanic or latino</td>\n",
+       "      <td>female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Endometrium</td>\n",
+       "      <td>39.76</td>\n",
+       "      <td>-21184.50</td>\n",
+       "      <td>21185.0</td>\n",
+       "      <td>3.5</td>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid adenoma, NOS</td>\n",
+       "      <td>Stage IA</td>\n",
+       "      <td>white</td>\n",
+       "      <td>not hispanic or latino</td>\n",
+       "      <td>female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>Endometrium</td>\n",
+       "      <td>51.19</td>\n",
+       "      <td>-18262.50</td>\n",
+       "      <td>18263.0</td>\n",
+       "      <td>4.5</td>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid adenoma, NOS</td>\n",
+       "      <td>Stage IA</td>\n",
+       "      <td>white</td>\n",
+       "      <td>not hispanic or latino</td>\n",
+       "      <td>female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>Endometrium</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>Carcinosarcoma, NOS</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>Endometrium</td>\n",
+       "      <td>32.69</td>\n",
+       "      <td>-27393.75</td>\n",
+       "      <td>27394.0</td>\n",
+       "      <td>3.5</td>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid adenoma, NOS</td>\n",
+       "      <td>Stage IA</td>\n",
+       "      <td>white</td>\n",
+       "      <td>not hispanic or latino</td>\n",
+       "      <td>female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>...</th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>99</th>\n",
+       "      <td>Endometrium</td>\n",
+       "      <td>29.40</td>\n",
+       "      <td>-27393.75</td>\n",
+       "      <td>27394.0</td>\n",
+       "      <td>4.2</td>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>Endometrioid adenoma, NOS</td>\n",
+       "      <td>Stage IA</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>100</th>\n",
+       "      <td>Endometrium</td>\n",
+       "      <td>35.42</td>\n",
+       "      <td>-27028.50</td>\n",
+       "      <td>27029.0</td>\n",
+       "      <td>1.5</td>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>Endometrioid adenoma, NOS</td>\n",
+       "      <td>Stage III</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>101</th>\n",
+       "      <td>Endometrium</td>\n",
+       "      <td>24.32</td>\n",
+       "      <td>-31046.25</td>\n",
+       "      <td>31047.0</td>\n",
+       "      <td>3.8</td>\n",
+       "      <td>United States</td>\n",
+       "      <td>Serous carcinoma, NOS</td>\n",
+       "      <td>Stage III</td>\n",
+       "      <td>black or african american</td>\n",
+       "      <td>not hispanic or latino</td>\n",
+       "      <td>female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>102</th>\n",
+       "      <td>Endometrium</td>\n",
+       "      <td>34.06</td>\n",
+       "      <td>-25567.50</td>\n",
+       "      <td>25568.0</td>\n",
+       "      <td>5.0</td>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>Serous carcinoma, NOS</td>\n",
+       "      <td>Stage IA</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>103</th>\n",
+       "      <td>Endometrium</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>Serous carcinoma, NOS</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "<p>104 rows × 12 columns</p>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "    tissue_or_organ_of_origin    bmi  days_to_birth  age_at_diagnosis  \\\n",
+       "0                 Endometrium  38.88      -23376.00           23376.0   \n",
+       "1                 Endometrium  39.76      -21184.50           21185.0   \n",
+       "2                 Endometrium  51.19      -18262.50           18263.0   \n",
+       "3                 Endometrium    NaN            NaN               NaN   \n",
+       "4                 Endometrium  32.69      -27393.75           27394.0   \n",
+       "..                        ...    ...            ...               ...   \n",
+       "99                Endometrium  29.40      -27393.75           27394.0   \n",
+       "100               Endometrium  35.42      -27028.50           27029.0   \n",
+       "101               Endometrium  24.32      -31046.25           31047.0   \n",
+       "102               Endometrium  34.06      -25567.50           25568.0   \n",
+       "103               Endometrium    NaN            NaN               NaN   \n",
+       "\n",
+       "     tumor_largest_dimension_diameter country_of_birth  \\\n",
+       "0                                 2.9    United States   \n",
+       "1                                 3.5    United States   \n",
+       "2                                 4.5    United States   \n",
+       "3                                 NaN              NaN   \n",
+       "4                                 3.5    United States   \n",
+       "..                                ...              ...   \n",
+       "99                                4.2          Ukraine   \n",
+       "100                               1.5          Ukraine   \n",
+       "101                               3.8    United States   \n",
+       "102                               5.0          Ukraine   \n",
+       "103                               NaN          Ukraine   \n",
+       "\n",
+       "             primary_diagnosis figo_stage                       race  \\\n",
+       "0    Endometrioid adenoma, NOS   Stage IA                      white   \n",
+       "1    Endometrioid adenoma, NOS   Stage IA                      white   \n",
+       "2    Endometrioid adenoma, NOS   Stage IA                      white   \n",
+       "3          Carcinosarcoma, NOS        NaN                        NaN   \n",
+       "4    Endometrioid adenoma, NOS   Stage IA                      white   \n",
+       "..                         ...        ...                        ...   \n",
+       "99   Endometrioid adenoma, NOS   Stage IA                        NaN   \n",
+       "100  Endometrioid adenoma, NOS  Stage III                        NaN   \n",
+       "101      Serous carcinoma, NOS  Stage III  black or african american   \n",
+       "102      Serous carcinoma, NOS   Stage IA                        NaN   \n",
+       "103      Serous carcinoma, NOS        NaN                        NaN   \n",
+       "\n",
+       "                  ethnicity  gender tumor_focality  \n",
+       "0    not hispanic or latino  female       Unifocal  \n",
+       "1    not hispanic or latino  female       Unifocal  \n",
+       "2    not hispanic or latino  female       Unifocal  \n",
+       "3                       NaN     NaN            NaN  \n",
+       "4    not hispanic or latino  female       Unifocal  \n",
+       "..                      ...     ...            ...  \n",
+       "99                      NaN  female       Unifocal  \n",
+       "100                     NaN  female       Unifocal  \n",
+       "101  not hispanic or latino  female       Unifocal  \n",
+       "102                     NaN  female       Unifocal  \n",
+       "103                     NaN     NaN            NaN  \n",
+       "\n",
+       "[104 rows x 12 columns]"
+      ]
+     },
+     "execution_count": 21,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "harmonized_dataset = bdi.materialize_mapping(dataset, harmonization_spec)\n",
+    "harmonized_dataset"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "For comparisson, here is how our original data looked like:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 22,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Tumor_Site</th>\n",
+       "      <th>BMI</th>\n",
+       "      <th>Age</th>\n",
+       "      <th>Age</th>\n",
+       "      <th>Tumor_Size_cm</th>\n",
+       "      <th>Country</th>\n",
+       "      <th>Histologic_type</th>\n",
+       "      <th>FIGO_stage</th>\n",
+       "      <th>Race</th>\n",
+       "      <th>Ethnicity</th>\n",
+       "      <th>Gender</th>\n",
+       "      <th>Tumor_Focality</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>Anterior endometrium</td>\n",
+       "      <td>38.88</td>\n",
+       "      <td>64.0</td>\n",
+       "      <td>64.0</td>\n",
+       "      <td>2.9</td>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>Posterior endometrium</td>\n",
+       "      <td>39.76</td>\n",
+       "      <td>58.0</td>\n",
+       "      <td>58.0</td>\n",
+       "      <td>3.5</td>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>Other, specify</td>\n",
+       "      <td>51.19</td>\n",
+       "      <td>50.0</td>\n",
+       "      <td>50.0</td>\n",
+       "      <td>4.5</td>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>Carcinosarcoma</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>Other, specify</td>\n",
+       "      <td>32.69</td>\n",
+       "      <td>75.0</td>\n",
+       "      <td>75.0</td>\n",
+       "      <td>3.5</td>\n",
+       "      <td>United States</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>White</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>...</th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>99</th>\n",
+       "      <td>Other, specify</td>\n",
+       "      <td>29.40</td>\n",
+       "      <td>75.0</td>\n",
+       "      <td>75.0</td>\n",
+       "      <td>4.2</td>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>100</th>\n",
+       "      <td>Other, specify</td>\n",
+       "      <td>35.42</td>\n",
+       "      <td>74.0</td>\n",
+       "      <td>74.0</td>\n",
+       "      <td>1.5</td>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>Endometrioid</td>\n",
+       "      <td>II</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>101</th>\n",
+       "      <td>Other, specify</td>\n",
+       "      <td>24.32</td>\n",
+       "      <td>85.0</td>\n",
+       "      <td>85.0</td>\n",
+       "      <td>3.8</td>\n",
+       "      <td>United States</td>\n",
+       "      <td>Serous</td>\n",
+       "      <td>II</td>\n",
+       "      <td>Black or African American</td>\n",
+       "      <td>Not-Hispanic or Latino</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>102</th>\n",
+       "      <td>Other, specify</td>\n",
+       "      <td>34.06</td>\n",
+       "      <td>70.0</td>\n",
+       "      <td>70.0</td>\n",
+       "      <td>5.0</td>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>Serous</td>\n",
+       "      <td>IA</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>Female</td>\n",
+       "      <td>Unifocal</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>103</th>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>Ukraine</td>\n",
+       "      <td>Serous</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "<p>104 rows × 12 columns</p>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "                Tumor_Site    BMI   Age   Age  Tumor_Size_cm        Country  \\\n",
+       "0     Anterior endometrium  38.88  64.0  64.0            2.9  United States   \n",
+       "1    Posterior endometrium  39.76  58.0  58.0            3.5  United States   \n",
+       "2           Other, specify  51.19  50.0  50.0            4.5  United States   \n",
+       "3                      NaN    NaN   NaN   NaN            NaN            NaN   \n",
+       "4           Other, specify  32.69  75.0  75.0            3.5  United States   \n",
+       "..                     ...    ...   ...   ...            ...            ...   \n",
+       "99          Other, specify  29.40  75.0  75.0            4.2        Ukraine   \n",
+       "100         Other, specify  35.42  74.0  74.0            1.5        Ukraine   \n",
+       "101         Other, specify  24.32  85.0  85.0            3.8  United States   \n",
+       "102         Other, specify  34.06  70.0  70.0            5.0        Ukraine   \n",
+       "103                    NaN    NaN   NaN   NaN            NaN        Ukraine   \n",
+       "\n",
+       "    Histologic_type FIGO_stage                       Race  \\\n",
+       "0      Endometrioid         IA                      White   \n",
+       "1      Endometrioid         IA                      White   \n",
+       "2      Endometrioid         IA                      White   \n",
+       "3    Carcinosarcoma        NaN                        NaN   \n",
+       "4      Endometrioid         IA                      White   \n",
+       "..              ...        ...                        ...   \n",
+       "99     Endometrioid         IA                        NaN   \n",
+       "100    Endometrioid         II                        NaN   \n",
+       "101          Serous         II  Black or African American   \n",
+       "102          Serous         IA                        NaN   \n",
+       "103          Serous        NaN                        NaN   \n",
+       "\n",
+       "                  Ethnicity  Gender Tumor_Focality  \n",
+       "0    Not-Hispanic or Latino  Female       Unifocal  \n",
+       "1    Not-Hispanic or Latino  Female       Unifocal  \n",
+       "2    Not-Hispanic or Latino  Female       Unifocal  \n",
+       "3                       NaN     NaN            NaN  \n",
+       "4    Not-Hispanic or Latino  Female       Unifocal  \n",
+       "..                      ...     ...            ...  \n",
+       "99                      NaN  Female       Unifocal  \n",
+       "100                     NaN  Female       Unifocal  \n",
+       "101  Not-Hispanic or Latino  Female       Unifocal  \n",
+       "102                     NaN  Female       Unifocal  \n",
+       "103                     NaN     NaN            NaN  \n",
+       "\n",
+       "[104 rows x 12 columns]"
+      ]
+     },
+     "execution_count": 22,
+     "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
+}


Test patch:
diff --git a/tests/test_api.py b/tests/test_api.py
index e69de29b..b9bc601b 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -0,0 +1,289 @@
+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():
+    # 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"
+
+
+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 "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]["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():
+    # 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 "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]["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]["target"].tolist()
+    assert "race" in df_matches[df_filter]["target"].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 = pd.Series(["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 = [
+        {
+            "source": "column_str_1",
+            "target": "string column 1",
+            "mapper": IdentityValueMapper(),
+        },
+        {
+            "source": "column_str_2",
+            "target": "string column 2",
+            "mapper": FunctionValueMapper(function=lambda x: x.upper()),
+        },
+    ]
+
+    # when
+    df_mapped = bdi.materialize_mapping(df_base, mapping_spec=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()
+
+
+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 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():
+    # 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
+    assert len(column_mappings.index) == 1
+
+    # 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"
+    )
+
+    # 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
+
+    # when: pass output of match_values() to materialize_mapping(),
+    df_mapped = bdi.materialize_mapping(df_source, value_mappings)
+
+    # 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]
diff --git a/tests/test_column_mapping.py b/tests/test_column_mapping.py
index c8755712..61dd2dd3 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(dataset=table1, global_table=table2)
+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()
+        # 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"
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()
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))