<jupyter_start><jupyter_text>T028 · Kinase similarity: Compare different perspectives**Note:** This talktorial is a part of TeachOpenCADD, a platform that aims to teach domain-specific skills and to provide pipeline templates as starting points for research projects.Authors:- <NAME>, 2021, [Volkamer lab, Charité](https://volkamerlab.org/)- <NAME>, 2021, [Volkamer lab, Charité](https://volkamerlab.org/)- <NAME>, 2021, [Volkamer lab, Charité](https://volkamerlab.org/)  Aim of this talktorialWe will compare different perspectives on kinase similarity, which were discussed in detail in previous notebooks:* **Talktorial T024**: Kinase pocket sequences (KLIFS pocket sequences)* **Talktorial T025**: Kinase pocket structures (KiSSim fingerprint based on KLIFS pocket residues)* **Talktorial T026**: Kinase-ligand interaction profiles (KLIFS IFPs based on KLIFS pocket residues)* **Talktorial T027**: Ligand profiling data (using ChEMBL29 bioactivity data)  Contents in *Theory** Kinase dataset* Kinase similarity descriptor (considering 4 different methods)* Distance matrix conditions  Contents in *Practical** Load kinase similarity and distance matrices* Distance matrix conditions* Visualize similarity for example perspective  * Visualize kinase distance matrix as heatmap  * Visualize similarity as dendrogram* Visualize similarities from the four different perspectives  * Preprocess distance matrices    * Normalize matrices    * Define kinase order  * Visualize kinase similarities  * Analysis of results  References* Kinase dataset: [Molecules (2021), 26(3), 629](https://www.mdpi.com/1420-3049/26/3/629) * Clustering and dendrograms with `scipy`: https://docs.scipy.org/doc/scipy/reference/cluster.hierarchy.html* Distance matrix:    * https://en.wikipedia.org/wiki/Distance_matrix    * Gil<NAME>. and <NAME>. "If it ain't broke, don't fix it: Sparse metric repair." _2017 55th Annual Allerton Conference on Communication, Control, and Computing (Allerton)_. IEEE, 2017. https://arxiv.org/pdf/1710.10655.pdf  Theory  Kinase dataset We use the kinase selection as defined in __Talktorial T023__.  Kinase similarity descriptor (considering 4 different methods)- __Talktorial T024__ = KLIFS pocket sequence- __Talktorial T025__ = KiSSim fingerprint- __Talktorial T026__ = KLIFS interaction fingerprint- __Talktorial T027__ = Ligand profile: ChEMBL29, bioactivityPlease refer to the talktorials for more details on each method.  Distance matrix conditionsA square, real matrix $M \in \mathbb{R}^{n \times n}$ is a distance matrix if $M$ satisfies the following conditions (following the definition of distance) :1. a. Positivity: $M[x, y] \geq 0$ for all $x, y$ and b. Null condition: $M[x, y] = 0 \iff x = y.$   2. Symmetry: $M[x, y] = M[y, x]$ for all $x, y.$3. Triangular inequality: $M[x, z] \leq M[x, y] + M[y, z]$ for all $x, y, z.$_Technical note_: In some cases, the null condition can be relaxed and only the following has to be satisfied: $M[x, x] = 0, \forall x.$ In this case, the term is known as semi-metric. We will use this condition in this notebook.  Practical<jupyter_code>from pathlib import Path

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.spatial import distance_matrix, distance
from scipy.cluster import hierarchy
HERE = Path(_dh[-1])
DATA = HERE / "data"<jupyter_output><empty_output><jupyter_text>Load kinase similarity and distance matrices We define the paths to the kinase distance matrices.<jupyter_code>kinase_matrix_paths = {
    "sequence": "T024_kinase_similarity_sequence",
    "kissim": "T025_kinase_similarity_kissim",
    "ifp": "T026_kinase_similarity_ifp",
    "ligand-profile": "T027_kinase_similarity_ligand_profile",
}

# Distance matrices (for dendrogram)
kinase_distance_matrix_paths = {
    perspective: HERE / f"../{folder_name}/data/kinase_distance_matrix.csv"
    for perspective, folder_name in kinase_matrix_paths.items()
}<jupyter_output><empty_output><jupyter_text>We load the distance matrices which were generated in the previous notebooks.<jupyter_code>kinase_distance_matrices = {}
for kinsim_perspective, path in kinase_distance_matrix_paths.items():
    kinase_distance_matrices[kinsim_perspective] = pd.read_csv(path, index_col=0).round(6)<jupyter_output><empty_output><jupyter_text>Distance matrix conditionsWe now create functions to verify the distance conditions. We check the dimension of the matrix: it should be a 2D, square array.<jupyter_code>def check_dimensionality(matrix):
    """
    Checks whether the input is a 2D array (matrix) and square.

    Parameters
    ----------
    matrix : np.array
        The matrix for which the condition should be checked.

    Returns
    -------
    bool :
        True if the condition is met, False otherwise.
    """
    if len(matrix.shape) != 2:
        raise ValueError(f"The input is not a matrix, but an array of shape {matrix.shape}.")
    elif matrix.shape[0] != matrix.shape[1]:
        raise ValueError(f"The input is not a square matrix. Failing.")
    else:
        return True<jupyter_output><empty_output><jupyter_text>We check that all values of the matrix are positive.<jupyter_code>def check_positivity(matrix):
    """
    Checks whether all values of a matrix are positive.

    Parameters
    ----------
    matrix : np.array
        The matrix for which the condition should be checked.

    Returns
    -------
    bool :
        True if the condition is met, False otherwise.
    """
    return (matrix >= 0).all()<jupyter_output><empty_output><jupyter_text>We check that the diagonal values of the matrix are zero.<jupyter_code>def check_null_diagonal(matrix):
    """
    Checks whether the diagonal entries of a matrix are all zero.

    Parameters
    ----------
    matrix : np.array
        The matrix for which the condition should be checked.

    Returns
    -------
    bool :
        True if the condition is met, False otherwise.
    """
    return (np.diagonal(matrix) == 0).all()<jupyter_output><empty_output><jupyter_text>We check that the matrix is symmetric.<jupyter_code>def check_symmetry(matrix):
    """
    Checks whether a matrix M is symmetric, i.e. M = M^T.

    Parameters
    ----------
    matrix : np.array
        The matrix for which the condition should be checked.

    Returns
    -------
    bool :
        True if the condition is met, False otherwise.
    """
    return (matrix == matrix.transpose()).all()<jupyter_output><empty_output><jupyter_text>Finally, we check the triangular inequality condition.<jupyter_code>def check_triangular_inequality(matrix, tolerance=0.11):
    """
    Checks whether the triangular inequality is met for a matrix,
    i.e. the shortest distance between two points is the straight line.

    Parameters
    ----------
    matrix : np.array
        The matrix for which the condition should be checked.
    tolerance : float
        The accepted tolerance for approximation.

    Returns
    -------
    bool :
        True if the condition is met, False otherwise.
    """
    triang_inequality = None
    for i in range(matrix.shape[0]):
        for j in range(matrix.shape[0]):
            for k in range(matrix.shape[0]):
                if matrix[i, j] <= matrix[i, k] + matrix[k, j] + tolerance:
                    triang_inequality = True
                else:
                    return False
    return triang_inequality<jupyter_output><empty_output><jupyter_text>We now create a function that combines all these conditions.<jupyter_code>def distance_condition(matrix):
    """
    Checks if all conditions for a matrix are met.

    Parameters
    ----------
    matrix : np.array
        The matrix for which the conditions should be checked.

    Returns
    -------
    None
    """

    dimensionality = check_dimensionality(matrix)
    if dimensionality:
        print(f"{'Dimensionality' : <30}" f"{'Satisfied'}")
    else:
        print(f"{'Dimensionality' : <30}" f"{'Not Satisfied'}")

    positivity = check_positivity(matrix)
    if positivity:
        print(f"{'Postivity' : <30}" f"{'Satisfied'}")
    else:
        print(f"{'Postivity' : <30}" f"{'Satisfied'}")

    diag = check_null_diagonal(matrix)
    if diag:
        print(f"{'Null diagonal' : <30}" f"{'Satisfied'}")
    else:
        print(f"{'Null diagonal' : <30}" f"{'Not Satisfied'}")

    symmetry = check_symmetry(matrix)
    if symmetry:
        print(f"{'Symmetry' : <30}" f"{'Satisfied'}")
    else:
        print(f"{'Symmetry' : <30}" f"{'Satisfied'}")

    triang_inequ = check_triangular_inequality(matrix)
    if triang_inequ:
        print(f"{'Triangular inequality' : <30}" f"{'Satisfied'}")
    else:
        print(f"{'Triangular inequality' : <30}" f"{'Not Satisfied'}")
    print("\n")
    return None
for descriptor, similarity_df in kinase_distance_matrices.items():
    print(f"{descriptor}")
    print("==========")
    x = distance_condition(similarity_df.values)<jupyter_output>sequence
==========
Dimensionality                Satisfied
Postivity                     Satisfied
Null diagonal                 Satisfied
Symmetry                      Satisfied
Triangular inequality         Satisfied


kissim
==========
Dimensionality                Satisfied
Postivity                     Satisfied
Null diagonal                 Satisfied
Symmetry                      Satisfied
Triangular inequality         Satisfied


ifp
==========
Dimensionality                Satisfied
Postivity                     Satisfied
Null diagonal                 Satisfied
Symmetry                      Satisfied
Triangular inequality         Satisfied


ligand-profile
==========
Dimensionality                Satisfied
Postivity                     Satisfied
Null diagonal                 Satisfied
Symmetry                      Satisfied
Triangular inequality         Not Satisfied<jupyter_text>If one of the following checks (dimensionality, positivity, null diagonal, or symmetry) fails, the rest of the notebook will not run properly and the matrix generation from the previous notebooks should be investigated.The triangular inequality does not have to be satisfied in order for the notebook to run, as is the case for ligand-profile. The reason for its failing is due to the function that describes the (dis)similarity between two kinases, which is not a distance (see __Talktorial T027__).  Visualize similarity for example perspective<jupyter_code>print(f"Choices of precalculated descriptors: {kinase_distance_matrix_paths.keys()}")<jupyter_output>Choices of precalculated descriptors: dict_keys(['sequence', 'kissim', 'ifp', 'ligand-profile'])<jupyter_text>We look at an example matrix:<jupyter_code>descriptor_selection = "sequence"
kinase_distance_matrix = kinase_distance_matrices[descriptor_selection]
kinase_distance_matrix<jupyter_output><empty_output><jupyter_text>Visualize kinase distance matrix as heatmapWe visualize the kinase matrix in the form of a heatmap.<jupyter_code>sns.heatmap(
    kinase_distance_matrix,
    linewidths=0,
    annot=True,
    square=True,
    cbar_kws={"label": "Kinase distance"},
    cmap="viridis",
)
plt.show()<jupyter_output><empty_output><jupyter_text>Visualize similarity as dendrogram Since distance matrices are symmetric, we use the `scipy` function `squareform` to create a condensed vector of the distance matrix of shape $n*(n-1)/2$, where $n$ is the shape of the distance matrix. The values in this vector correspond to the values of the lower triangular matrix.<jupyter_code>D = kinase_distance_matrix.values
D_condensed = distance.squareform(D)
D_condensed<jupyter_output><empty_output><jupyter_text>We can submit this condensed vector to a hierarchical clustering to extract the relationship between the different kinases. We use here `method="average"`, which stands for the linkage method UPGMA (unweighted pair group method with arithmetic mean). This means that the distance between two clusters A and B is defined as the average of all distances between pairs of elements in clusters A and B. At each clustering step, the two clusters with the lowest average distance are combined.<jupyter_code>hclust = hierarchy.linkage(D_condensed, method="average")<jupyter_output><empty_output><jupyter_text>We now generate a phylogenetic tree based on the clustering.<jupyter_code>tree = hierarchy.to_tree(hclust)<jupyter_output><empty_output><jupyter_text>We visualize the tree as a dendrogram.<jupyter_code>fig, ax = plt.subplots()
labels = kinase_distance_matrix.columns.to_list()
hierarchy.dendrogram(hclust, labels=labels, orientation="left", ax=ax)
ax.set_xlabel("Distance")
plt.show()<jupyter_output><empty_output><jupyter_text>Visualize similarities from the four different perspectives  Preprocess distance matrices  Normalize matrices We normalize the different matrices to values in $[0, 1]$.<jupyter_code>def _min_max_normalize(distance_matrix_df):
    """
    Apply min-max normalization to input DataFrame.

    Parameters
    ----------
    kinase_distance_matrix_df : pd.DataFrame
        Kinase distance matrix.

    Returns
    -------
    pd.DataFrame
        Normalized kinase distance matrix.
    """
    min_ = distance_matrix_df.min().min()
    max_ = distance_matrix_df.max().max()
    distance_matrix_normalized_df = (distance_matrix_df - min_) / (max_ - min_)
    return distance_matrix_normalized_df
kinase_distance_matrices_normalized = {}
for descriptor, score_df in kinase_distance_matrices.items():
    score_normalized_df = _min_max_normalize(score_df)
    kinase_distance_matrices_normalized[descriptor] = score_normalized_df<jupyter_output><empty_output><jupyter_text>Define kinase orderDefine in which order to display the kinases.<jupyter_code>kinase_names = kinase_distance_matrices_normalized["sequence"].columns
def _define_kinase_order(kinase_distance_matrix_df, kinase_names):
    """
    Define the order in which kinases shall
    appear in the input DataFrame.

    Parameters
    ----------
    kinase_distance_matrix_df : pd.DataFrame
        Kinase distance matrix.
    kinase_name : list of str
        List of kinase names to be used for sorting.

    Returns
    -------
    pd.DataFrame
        Kinase distance matrix with sorted columns/rows.
    """
    kinase_distance_matrix_df = kinase_distance_matrix_df.reindex(kinase_names, axis=1).reindex(
        kinase_names, axis=0
    )
    return kinase_distance_matrix_df
kinase_distance_matrices_normalized = {
    descriptor: _define_kinase_order(score_df, kinase_names)
    for descriptor, score_df in kinase_distance_matrices_normalized.items()
}<jupyter_output><empty_output><jupyter_text>Visualize kinase similarities<jupyter_code>def heatmap(score_df, ax=None, title=""):
    """
    Generate a heatmap from a matrix.

    Parameters
    ----------
    score_df : pd.DataFrame
        Distance or similarity score matrix.
    ax : matplotlib.axes
        Plot axis to use!
    title : str
        Plot title.
    """
    sns.heatmap(score_df, linewidths=0, annot=True, square=True, cmap="viridis", ax=ax)
def dendrogram(distance_matrix, ax=None, title=""):
    """
    Generate a dendrogram from a distance matrix.

    Parameters
    ----------
    distance_matrix : pd.DataFrame
        Distance matrix.
    ax : matplotlib.axes
        Plot axis to use!
    title : str
        Plot title.
    """
    D = distance_matrix.values
    D_condensed = distance.squareform(D)
    hclust = hierarchy.linkage(D_condensed, method="average")
    tree = hierarchy.to_tree(hclust)
    labels = distance_matrix.columns.to_list()
    hierarchy.dendrogram(hclust, labels=labels, orientation="left", ax=ax)
    ax.set_title(title)
    ax.set_xlabel("Distance")<jupyter_output><empty_output><jupyter_text>Note for ease of interpretability, we show below:* Heatmaps based on the **similarity** matrix (the higher the value, the higher the similarity).* Dendrograms calculated based on the **distance** matrix, where clusters describe the similarity between kinases.<jupyter_code>n_perspectives = len(kinase_distance_matrices_normalized)
fig, axes = plt.subplots(2, n_perspectives, figsize=(n_perspectives * 5, 10))
for i, (perspective, distance_matrix) in enumerate(kinase_distance_matrices_normalized.items()):
    # heatmap based on similarity matrix
    similarity_matrix = 1 - distance_matrix
    heatmap(similarity_matrix, ax=axes[0][i], title=perspective)
    # dendrogram based on distance matrix
    dendrogram(distance_matrix, ax=axes[1][i], title=perspective)<jupyter_output><empty_output><jupyter_text>We load here again the input kinase information that we defined in __Talktorial T023__, to determine, among other, to which group each kinase belongs to.<jupyter_code>kinase_selection_df = pd.read_csv(HERE / "../T023_what_is_a_kinase/data/kinase_selection.csv")
kinase_selection_df<jupyter_output><empty_output><jupyter_text>Analysis of results- The _sequence_ approach reproduces nicely the Manning tree relationships.  - The TK kinases EGFR, ErbB2, MET, and KDR are grouped together with highest similarities for EGFR and ErbB2.  - The TKL kinase LCK groups next to the TK kinases.  - The CMGC kinases p38a and CDK2 are grouped together.  - The atypical kinase p110a is a singleton, i.e., forms its own cluster.- When taking into account the pocket structure (physicochemical and spatial properties) with the _kissim_ approach, similar observations can be made:   - The typical kinases are grouped together, while the atypical kinase p110a is a singleton.   - Within the typical kinase grouping, EGFR and ErbB2 are the most similar (but not as prominent as in the _sequence_ approach).  - Kinases from different groups, e.g. CDK2 (CMGC) and BRAF (TKL), cluster together.- The _ifp_ method:  - BRAF and the atypical kinase p110a form a singleton each.  - Surprisingly, ErbB2 forms a singleton (=not grouped with EGFR), but we have to keep in mind that we have the least data points ($4$) for ErbB2.- The _ligand-profile_ method:  - Interestingly, CDK2 and p110a form their own cluster early on (i.e. low similarity to the rest).  - BRAF (TKL), p38a (CMGC), KDR and LCK (both TK) as well as EGFR and ErbB2 (TK) form one cluster each.  - Surprisingly, MET (TK) forms a singleton (not clustering with any of the other TK kinases).- Overall observations:  - Members of the same family (here: EGFR and ErbB2) are grouped together closely in the _sequence_, _kissim_, and _ligand-profile_ approaches.  - The atypical kinase p110a shows high dissimilarity in the _sequence_, _kissim_, and _ifp_ approaches.  - Unexpectedly, KDR (TK) and p38a (CMGC) cluster together in all views (except for _sequence_). Thus, it would be interesting to explore this relationship further.<!---- Comparison to multi-target profiles in [Molecules (2021), 26(3), 629](https://www.mdpi.com/1420-3049/26/3/629)  - Profile 1:     - EGFR/ErbB2 as on-targets and BRAF as anti-target    - Should be doable from "sequence", "ligand-profile" view (less clear from "kisim" and "ifp" view)  - Profile 2:    - EGFR/p110a as on-targets and BRAF as anti-target    - Should be hard to design from all views  - Profile 3:     - EGFR/KDR as on-targets and BRAF as anti-target    - Should be hard to design except from a "sequence" view--->  DiscussionWe have assessed kinase similarity from many different angles: the pocket sequence, the pocket structure, ligand binding modes (interaction fingerprints), and ligand bioactivities. We see from our results that different conclusions can be drawn for each method, while some observations agree.Thus, it is beneficial to consider different strategies to study kinase similarity.The different approaches have strengths and drawbacks:- The _sequence_ method: Sequence data is available for all kinases, however no information about structure or ligand binding is included.- The _kissim_, _ifp_, and _ligand-profile_ methods: Information about structure, ligand binding, or ligand bioactivity is included, however some kinases may not have any data, some kinases may have only few data, and most kinases may not be fully explored (even if a lot of data is available).- All methods rely on simplifications!   - Dealing with multiple measurements    - _ligand-profile_ method: Only one measurement per kinase-ligand is used here.    - _kissim_ and _ifp_ method: Only one structure/IFP pair represents one kinase pair here.  - Bioactivity cutoffs to define active/inactive compounds in case of the _ligand-profile_ method.  Quiz1. Can you think of another perspective to assess kinase similarity?2. Are there other methods to assess and visualize the similarities?3. Can you name at least one advantage and one disadvantage per perspective discussed here?4. Could the different methods be applied to other proteins in general?5. What is the difference between the distances that we show in the heatmap and the dendrogram?   Appendix In this appendix, we generate individual plots (both heatmap and dendrogram) for the sequence perspective. The plots are saved in svg format.  Sequence heatmap in svg<jupyter_code>fig, ax = plt.subplots()
sns.heatmap(
    1 - kinase_distance_matrices_normalized["sequence"],
    linewidths=0,
    annot=True,
    square=True,
    cbar_kws={"label": "Kinase similarity"},
    cmap="viridis",
)
plt.title("sequence")
plt.savefig(DATA / "heatmap_sequence.svg")<jupyter_output><empty_output><jupyter_text>Sequence dendrogram in svg<jupyter_code>fig, ax = plt.subplots()
D = kinase_distance_matrices_normalized["sequence"].values
D_condensed = distance.squareform(D)
hclust = hierarchy.linkage(D_condensed, method="average")
tree = hierarchy.to_tree(hclust)
labels = distance_matrix.columns.to_list()
hierarchy.dendrogram(hclust, labels=labels, orientation="left", ax=ax)
ax.set_title("sequence")
ax.set_xlabel("Distance")
ax.set_aspect(1 / 100)
plt.savefig(DATA / "dendrogram_sequence.svg")<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09048533040855497, 'fraction_numerical': 0.01110501782286811, 'mean_word_length': 3.9938384299406664, 'pattern_counts': {'":': 6, '<': 108, '<?xml version=': 0, '>': 98, 'https://': 8, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '23', 'dir': 'jupyter-structured-clean-dedup', 'id': '589241', 'license': 'CC-BY-4.0', 'n_tokens_mistral': 6678, 'n_tokens_neox': 6098, 'n_words': 2167, 'path': 'teachopencadd/talktorials/T028_kinase_similarity_compare_perspectives/talktorial.ipynb', 'repo_name': 'volkamerlab/TeachOpenCADD', 'size': '21914'}