Trajectory Inference for Three Slices of the Mouse Thymus Dataset

Loading Packages

[1]:
import warnings
warnings.filterwarnings('ignore')

import scanpy as sc
import numpy as np

import os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"

Loading Data

[2]:
def load_data(replicate):
    """
    Load data from files for each replicate and store them in a dictionary.

    Parameters:
    - replicate: list of strings, the replicate identifiers.

    Returns:
    - adata_analysis: dictionary, where keys are replicate identifiers and values are AnnData objects.
    """
    adata_analysis = {}
    for rep in replicate:
        file_path = f'../../../Mouse_Thymus_Replicate{rep}.h5ad'
        adata_analysis[f'replicate_{rep}'] = sc.read_h5ad(file_path)
    return adata_analysis

replicate = ['1', '2', '3']
adata_analysis = load_data(replicate)

Calculating UMAP

[3]:
def calculate_umap(adata, methods):
    """
    Calculate UMAP for each method and store the results in the AnnData object.

    Parameters:
    - adata: AnnData object, the data to process.
    - methods: list of strings, the methods to calculate UMAP for.
    """
    for method in methods:
        if method in adata.obsm:
            print(method)
            sc.pp.neighbors(adata, use_rep=method)
            # print(" run1")
            sc.tl.umap(adata, min_dist=0.5, spread=0.5)
            # print(" run2")
            adata.obsm[f'{method}_UMAP'] = adata.obsm['X_umap']
            # print(" run3")
            del adata.obsm['X_umap']
        else:
            print(f"Method '{method}' not found in obsm.")

methods_analysis = ['SpatialCOC', 'SpatialGlue', 'Seurat', 'STAGATE']

Calculating Centroids

[4]:
def calculate_centroids(adata, methods):
    """
    Calculate centroids for each cluster of each method.

    Parameters:
    - adata: AnnData object, the data to process.
    - methods: list of strings, the methods to calculate centroids for.

    Returns:
    - centroids_dict: dictionary, where keys are method names and values are dictionaries of centroids.
    """
    centroids_dict = {}
    for method in methods:
        umap_key = f'{method}_UMAP'
        if umap_key in adata.obsm:
            unique_clusters = np.unique(adata.obs[method])
            centroids = {}
            for cluster in unique_clusters:
                umap_coords = adata[adata.obs[method] == cluster].obsm[umap_key]
                centroid = np.mean(umap_coords, axis=0)
                centroids[cluster] = centroid
            centroids_dict[method] = centroids
        else:
            print(f"UMAP for method '{method}' not found in obsm.")
            centroids_dict[method] = None
    return centroids_dict

PAGA Analysis

[5]:
def perform_paga(adata, methods):
    """
    Perform PAGA for each method and store the results in the AnnData object.

    Parameters:
    - adata: AnnData object, the data to process.
    - methods: list of tuples, where each tuple contains the cluster key and the embedding key.
    """
    for cluster_key, embed_key in methods:
        sc.pp.neighbors(
            adata,
            use_rep=embed_key,
            key_added=f'neighbors_{cluster_key}',
            n_neighbors=50
        )
        sc.tl.paga(
            adata,
            groups=cluster_key,
            neighbors_key=f'neighbors_{cluster_key}'
        )
        adata.uns[f'paga_{cluster_key}'] = adata.uns['paga'].copy()

methods = [
    ('SpatialCOC', 'SpatialCOC'),
    ('SpatialGlue', 'SpatialGlue'),
    ('Seurat', 'Seurat'),
    ('STAGATE', 'STAGATE'),
]

Ploting Function

[6]:
def plot_umap_with_centroids(adata, methods, replicate, paga_thresholds, colors_by_replicate_and_method):
    """
    Plot UMAP with centroids and PAGA connections for each method in the given replicate.

    Parameters:
    adata (AnnData): The AnnData object containing the data.
    methods (list): A list of tuples, where each tuple contains the cluster key and embed key for a method.
    replicate (str): The current replicate being processed.
    paga_thresholds (dict): A dictionary where keys are method names and values are the corresponding PAGA thresholds.
    colors_by_replicate_and_method (dict): A dictionary where keys are replicate numbers, and values are dictionaries with method names as keys and color lists as values.
    """
    all_connects = {}
    for cluster_key, embed_key in methods:
        print(cluster_key)
        umap_key = f'{embed_key}_UMAP'
        if umap_key not in adata.obsm or cluster_key not in adata.obs:
            print(f"Skipping {cluster_key} - data not found")
            continue

        # Get the color list corresponding to the current replicate and method
        colors = colors_by_replicate_and_method[replicate][cluster_key]

        unique_clusters = np.unique(adata.obs[cluster_key])
        centroids = {
            cluster: np.mean(adata[adata.obs[cluster_key] == cluster].obsm[umap_key], axis=0)
            for cluster in unique_clusters
        }
        print(f"Plot for {cluster_key} in replicate {replicate}:")
        fig, ax = plt.subplots(figsize=(6, 6))
        ax.scatter(
            adata.obsm[umap_key][:, 0],
            adata.obsm[umap_key][:, 1],
            c=[colors[int(x) % len(colors)] for x in adata.obs[cluster_key]],
            s=100,
            alpha=0.7
        )

        # Plot centroids
        for cluster, centroid in centroids.items():
            ax.scatter(centroid[0], centroid[1], marker='o', color='black', s=800)

        # Plot PAGA connections
        paga_key = f'paga_{cluster_key}'
        if paga_key in adata.uns:
            connectivities = adata.uns[paga_key]['connectivities'].toarray()
            all_connects[cluster_key] = connectivities
            cluster_indices = {cluster: i for i, cluster in enumerate(unique_clusters)}
            print(f" {paga_key=} {connectivities.shape=}")
            threshold = paga_thresholds.get(cluster_key, 0.1)
            for i in range(len(unique_clusters)):
                for j in range(i + 1, len(unique_clusters)):
                    if connectivities[i, j] > threshold:
                        start = centroids[unique_clusters[i]]
                        end = centroids[unique_clusters[j]]
                        linewidth = 2 + 8 * connectivities[i, j]
                        ax.plot([start[0], end[0]], [start[1], end[1]],
                               color='black', linewidth=linewidth, alpha=1)

        # Remove axis labels, ticks, and grid
        ax.set_title(f'')
        ax.set_xlabel('')
        ax.set_ylabel('')
        ax.set_xticks([])
        ax.set_yticks([])
        ax.grid(False)
        for spine in ax.spines.values():
            spine.set_visible(False)

        # Save and show the plot
        plt.tight_layout()
        # plt.savefig(f'{cluster_key}_rep_{replicate}.png', dpi=500)
        # plt.savefig(f'{cluster_key}_rep_{replicate}.eps')
        plt.show()
    return all_connects

Running

[7]:
colors_by_replicate_and_method = {
    # Replicate 1
    '1': {
        'SpatialCOC':  ['#fdf0d5', '#264653', '#83c5be', '#2a9d8f', '#f9c74f', '#99582a', '#ee6055'],
        'SpatialGlue': ['#f9c74f', '#83c5be', '#2a9d8f', '#fdf0d5', '#264653', '#ee6055', '#99582a'],
        'Seurat':      ['#83c5be', '#f9c74f', '#99582a', '#2a9d8f', '#fdf0d5', '#ee6055', '#264653'],
        'STAGATE':     ['#99582a', '#2a9d8f', '#83c5be', '#fdf0d5', '#264653', '#f9c74f', '#ee6055'],
    },
    # Replicate 2
    '2': {
        'SpatialCOC':  ['#264653', '#ee6055', '#83c5be', '#99582a', '#fdf0d5', '#2a9d8f', '#f9c74f'],
        'SpatialGlue': ['#264653', '#fdf0d5', '#99582a', '#f9c74f', '#83c5be', '#ee6055', '#2a9d8f'],
        'Seurat':      ['#83c5be', '#ee6055', '#99582a', '#264653', '#f9c74f', '#fdf0d5', '#2a9d8f'],
        'STAGATE':     ['#fdf0d5', '#99582a', '#f9c74f', '#2a9d8f', '#83c5be', '#264653', '#ee6055'],
    },
    # Replicate 3
    '3': {
        'SpatialCOC':  ['#264653', '#ee6055', '#99582a', '#f9c74f', '#83c5be', '#fdf0d5', '#2a9d8f'],
        'SpatialGlue': ['#f9c74f', '#99582a', '#fdf0d5', '#2a9d8f', '#83c5be', '#ee6055', '#264653'],
        'Seurat':      ['#2a9d8f', '#ee6055', '#99582a', '#264653', '#fdf0d5', '#f9c74f', '#83c5be'],
        'STAGATE':     ['#264653', '#2a9d8f', '#ee6055', '#fdf0d5', '#83c5be', '#99582a', '#f9c74f'],
    },
}

# Set PAGA thresholds for each method
paga_thresholds = {
    'SpatialCOC': 0.45,
    'SpatialGlue': 0.2,
    'Seurat': 0.1,
    'STAGATE': 0.2
}

graphs = {}
# Run
for rep in replicate:
    print(f"Processing replicate {rep}")
    adata = adata_analysis[f'replicate_{rep}']
    # print("run")
    calculate_umap(adata, methods_analysis)
    # print("run2")
    centroids_dict = calculate_centroids(adata, methods_analysis)
    # print("run3")
    perform_paga(adata, methods)
    # print("run4")
    all_connects = plot_umap_with_centroids(
        adata,
        methods,
        rep,
        paga_thresholds,
        colors_by_replicate_and_method
    )
    graphs[rep] = all_connects
Processing replicate 1
SpatialCOC
SpatialGlue
Seurat
STAGATE
SpatialCOC
Plot for SpatialCOC in replicate 1:
 paga_key='paga_SpatialCOC' connectivities.shape=(7, 7)
../../_images/Reproduction_Mouse_Thymus_Datasets_7_Trajectory_Inference_14_1.png
SpatialGlue
Plot for SpatialGlue in replicate 1:
 paga_key='paga_SpatialGlue' connectivities.shape=(7, 7)
../../_images/Reproduction_Mouse_Thymus_Datasets_7_Trajectory_Inference_14_3.png
Seurat
Plot for Seurat in replicate 1:
 paga_key='paga_Seurat' connectivities.shape=(7, 7)
../../_images/Reproduction_Mouse_Thymus_Datasets_7_Trajectory_Inference_14_5.png
STAGATE
Plot for STAGATE in replicate 1:
 paga_key='paga_STAGATE' connectivities.shape=(7, 7)
../../_images/Reproduction_Mouse_Thymus_Datasets_7_Trajectory_Inference_14_7.png
Processing replicate 2
SpatialCOC
SpatialGlue
Seurat
STAGATE
SpatialCOC
Plot for SpatialCOC in replicate 2:
 paga_key='paga_SpatialCOC' connectivities.shape=(7, 7)
../../_images/Reproduction_Mouse_Thymus_Datasets_7_Trajectory_Inference_14_9.png
SpatialGlue
Plot for SpatialGlue in replicate 2:
 paga_key='paga_SpatialGlue' connectivities.shape=(7, 7)
../../_images/Reproduction_Mouse_Thymus_Datasets_7_Trajectory_Inference_14_11.png
Seurat
Plot for Seurat in replicate 2:
 paga_key='paga_Seurat' connectivities.shape=(7, 7)
../../_images/Reproduction_Mouse_Thymus_Datasets_7_Trajectory_Inference_14_13.png
STAGATE
Plot for STAGATE in replicate 2:
 paga_key='paga_STAGATE' connectivities.shape=(7, 7)
../../_images/Reproduction_Mouse_Thymus_Datasets_7_Trajectory_Inference_14_15.png
Processing replicate 3
SpatialCOC
SpatialGlue
Seurat
STAGATE
SpatialCOC
Plot for SpatialCOC in replicate 3:
 paga_key='paga_SpatialCOC' connectivities.shape=(7, 7)
../../_images/Reproduction_Mouse_Thymus_Datasets_7_Trajectory_Inference_14_17.png
SpatialGlue
Plot for SpatialGlue in replicate 3:
 paga_key='paga_SpatialGlue' connectivities.shape=(7, 7)
../../_images/Reproduction_Mouse_Thymus_Datasets_7_Trajectory_Inference_14_19.png
Seurat
Plot for Seurat in replicate 3:
 paga_key='paga_Seurat' connectivities.shape=(7, 7)
../../_images/Reproduction_Mouse_Thymus_Datasets_7_Trajectory_Inference_14_21.png
STAGATE
Plot for STAGATE in replicate 3:
 paga_key='paga_STAGATE' connectivities.shape=(7, 7)
../../_images/Reproduction_Mouse_Thymus_Datasets_7_Trajectory_Inference_14_23.png

Saving Results

[8]:
def save_umap_clustering_to_excel(adata_dict, output_path):
    """
    Save UMAP coordinates and clustering results to Excel file

    Parameters:
    adata_dict: Dictionary with replicate names as keys and AnnData objects as values
    output_path: Path to save the Excel file
    """
    with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
        for replicate_name, adata in adata_dict.items():
            print(f"Processing {replicate_name}...")

            # Dictionary to store data for each method
            data_to_save = {}

            # Define mapping between UMAP coordinates and clustering labels
            pairs = [
                ('SpatialCOC_UMAP', 'SpatialCOC'),
                ('SpatialGlue_UMAP', 'SpatialGlue'),
                ('Seurat_UMAP', 'Seurat'),
                ('STAGATE_UMAP', 'STAGATE')
            ]

            # Extract UMAP coordinates and clustering labels for each method
            for umap_key, cluster_key in pairs:
                if umap_key in adata.obsm and cluster_key in adata.obs:
                    # Get UMAP coordinates
                    umap_data = adata.obsm[umap_key]

                    # Create DataFrame with UMAP coordinates and cluster labels
                    if umap_data.shape[1] >= 2:  # Ensure at least 2 dimensions
                        umap_df = pd.DataFrame(
                            umap_data[:, :2],  # Take first 2 UMAP dimensions
                            columns=[f'{umap_key}_1', f'{umap_key}_2']
                        )

                        # Add cluster labels
                        cluster_labels = adata.obs[cluster_key]
                        if isinstance(cluster_labels, pd.Series):
                            umap_df[cluster_key] = cluster_labels.values
                        else:
                            umap_df[cluster_key] = cluster_labels

                        # Set index to observation names
                        umap_df.index = adata.obs_names

                        data_to_save[f"{cluster_key}_data"] = umap_df

            # Combine all DataFrames for this replicate
            if data_to_save:
                # Start with the first DataFrame
                first_key = list(data_to_save.keys())[0]
                combined_df = data_to_save[first_key].copy()

                # Add columns from remaining DataFrames
                for key in list(data_to_save.keys())[1:]:
                    df = data_to_save[key]
                    for col in df.columns:
                        if col not in combined_df.columns:
                            combined_df[col] = df[col]

                # Write to Excel sheet
                sheet_name = replicate_name[:31]  # Truncate to 31 chars for Excel compatibility

                # Save DataFrame to Excel
                combined_df.to_excel(writer, sheet_name=sheet_name)

                print(f"  {replicate_name}: Saved {len(combined_df.columns)} columns")
            else:
                print(f"  {replicate_name}: No valid UMAP/clustering data found")

    print(f"\nResults saved to: {output_path}")

# save_umap_clustering_to_excel(adata_analysis, "umap_clustering_results.xlsx")

Batch effect removal quantification

[9]:
from scipy.spatial.distance import cosine
from itertools import permutations
import numpy as np

def graph_distance_with_permutation(adj1, adj2, permutation):
    """
    Calculate cosine distance between two adjacency matrices under a given permutation

    Parameters:
    - adj1: numpy array, first adjacency matrix
    - adj2: numpy array, second adjacency matrix
    - permutation: tuple, permutation to apply to adj2

    Returns:
    - distance: float, cosine distance between the two matrices
    """
    n = len(permutation)
    # Create permutation matrix from the permutation tuple
    perm_matrix = np.zeros((n, n))
    for i, j in enumerate(permutation):
        perm_matrix[i, j] = 1

    # Apply permutation to adj2: P * A * P^T
    adj2_permuted = perm_matrix @ adj2 @ perm_matrix.T

    # Flatten matrices and compute cosine distance
    vec1 = adj1.ravel()
    vec2 = adj2_permuted.ravel()

    return cosine(vec1, vec2)


def find_optimal_graph_matching(adj1, adj2):
    """
    Find optimal permutation that minimizes distance between two adjacency matrices

    Parameters:
    - adj1: numpy array, first adjacency matrix
    - adj2: numpy array, second adjacency matrix

    Returns:
    - best_permutation: tuple, permutation that gives minimum distance
    - min_distance: float, minimum distance achieved
    - all_distances: list, distances for all permutations
    """
    n = adj1.shape[0]
    identity = tuple(range(n))

    # Try all permutations (warning: factorial complexity!)
    all_perms = list(permutations(range(n)))
    min_distance = float('inf')
    best_permutation = identity
    all_distances = []

    for perm in all_perms:
        dist = graph_distance_with_permutation(adj1, adj2, perm)
        all_distances.append(dist)

        if dist < min_distance:
            min_distance = dist
            best_permutation = perm

    return best_permutation, min_distance, all_distances


def calculate_pairwise_graph_distances(graphs, methods):
    """
    Calculate pairwise distances between replicate graphs for each method

    Parameters:
    - graphs: dict, nested dictionary: graphs[replicate][method] = adjacency_matrix
    - methods: list, list of method names to analyze

    Returns:
    - results: dict, nested dictionary with distance results for each method and pair
    """
    replicates = sorted(graphs.keys())
    results = {}

    for method in methods:
        print(f"\n{'='*60}")
        print(f"Analyzing method: {method}")
        print(f"{'='*60}")

        method_results = {}

        # Compare each pair of replicates
        for i, rep1 in enumerate(replicates):
            for j, rep2 in enumerate(replicates):
                if i < j:  # Only compare each pair once
                    pair_key = f"Rep{rep1}_vs_Rep{rep2}"

                    adj1 = graphs[rep1][method]
                    adj2 = graphs[rep2][method]

                    print(f"\nComparing: Replicate {rep1} vs Replicate {rep2}")
                    print(f"Matrix 1 shape: {adj1.shape}, Matrix 2 shape: {adj2.shape}")

                    # Find optimal permutation
                    best_perm, min_dist, all_dists = find_optimal_graph_matching(adj1, adj2)

                    # Also compute unaligned distance (without permutation)
                    identity = tuple(range(adj1.shape[0]))
                    unaligned_dist = graph_distance_with_permutation(adj1, adj2, identity)

                    method_results[pair_key] = {
                        'best_permutation': best_perm,
                        'min_distance': min_dist,
                        'unaligned_distance': unaligned_dist,
                        'distance_reduction': unaligned_dist - min_dist,
                        'reduction_percentage': (unaligned_dist - min_dist) / unaligned_dist * 100 if unaligned_dist > 0 else 0
                    }

                    print(f"Best permutation: {best_perm}")
                    print(f"Minimum distance: {min_dist:.6f}")
                    # Optional detailed output:
                    # print(f"Unaligned distance: {unaligned_dist:.6f}")
                    # print(f"Distance reduction: {unaligned_dist - min_dist:.6f} ({(unaligned_dist - min_dist) / unaligned_dist * 100:.2f}%)")

        results[method] = method_results

    return results
[10]:
methods_to_analyze = ['SpatialCOC', 'SpatialGlue', 'Seurat', 'STAGATE']
graph_matching_results = calculate_pairwise_graph_distances(graphs, methods_to_analyze)

============================================================
Analyzing method: SpatialCOC
============================================================

Comparing: Replicate 1 vs Replicate 2
Matrix 1 shape: (7, 7), Matrix 2 shape: (7, 7)
Best permutation: (6, 1, 4, 5, 2, 0, 3)
Minimum distance: 0.032015

Comparing: Replicate 1 vs Replicate 3
Matrix 1 shape: (7, 7), Matrix 2 shape: (7, 7)
Best permutation: (6, 3, 5, 2, 1, 0, 4)
Minimum distance: 0.007364

Comparing: Replicate 2 vs Replicate 3
Matrix 1 shape: (7, 7), Matrix 2 shape: (7, 7)
Best permutation: (0, 3, 1, 4, 5, 2, 6)
Minimum distance: 0.035020

============================================================
Analyzing method: SpatialGlue
============================================================

Comparing: Replicate 1 vs Replicate 2
Matrix 1 shape: (7, 7), Matrix 2 shape: (7, 7)
Best permutation: (3, 5, 0, 6, 4, 1, 2)
Minimum distance: 0.063992

Comparing: Replicate 1 vs Replicate 3
Matrix 1 shape: (7, 7), Matrix 2 shape: (7, 7)
Best permutation: (0, 6, 3, 4, 5, 2, 1)
Minimum distance: 0.125532

Comparing: Replicate 2 vs Replicate 3
Matrix 1 shape: (7, 7), Matrix 2 shape: (7, 7)
Best permutation: (3, 0, 6, 2, 4, 1, 5)
Minimum distance: 0.154660

============================================================
Analyzing method: Seurat
============================================================

Comparing: Replicate 1 vs Replicate 2
Matrix 1 shape: (7, 7), Matrix 2 shape: (7, 7)
Best permutation: (2, 6, 3, 5, 0, 1, 4)
Minimum distance: 0.180691

Comparing: Replicate 1 vs Replicate 3
Matrix 1 shape: (7, 7), Matrix 2 shape: (7, 7)
Best permutation: (3, 1, 0, 6, 4, 5, 2)
Minimum distance: 0.150814

Comparing: Replicate 2 vs Replicate 3
Matrix 1 shape: (7, 7), Matrix 2 shape: (7, 7)
Best permutation: (0, 1, 4, 3, 2, 6, 5)
Minimum distance: 0.074865

============================================================
Analyzing method: STAGATE
============================================================

Comparing: Replicate 1 vs Replicate 2
Matrix 1 shape: (7, 7), Matrix 2 shape: (7, 7)
Best permutation: (1, 0, 6, 5, 2, 4, 3)
Minimum distance: 0.036836

Comparing: Replicate 1 vs Replicate 3
Matrix 1 shape: (7, 7), Matrix 2 shape: (7, 7)
Best permutation: (6, 2, 3, 0, 5, 1, 4)
Minimum distance: 0.056307

Comparing: Replicate 2 vs Replicate 3
Matrix 1 shape: (7, 7), Matrix 2 shape: (7, 7)
Best permutation: (2, 0, 5, 4, 1, 6, 3)
Minimum distance: 0.080665
[11]:
import pandas as pd

# Create a summary table from the graph matching results
summary_data = []

for method in methods_to_analyze:
    for pair_key, pair_result in graph_matching_results[method].items():
        summary_data.append({
            'Method': method,
            'Replicate_Pair': pair_key,
            'Min_Distance': pair_result['min_distance'],
            'Unaligned_Distance': pair_result['unaligned_distance'],
            'Distance_Reduction': pair_result['distance_reduction'],
            'Reduction_Percentage(%)': pair_result['reduction_percentage']
        })

summary_df = pd.DataFrame(summary_data)

print("\n" + "="*80)
print("GRAPH MATCHING RESULTS SUMMARY")
print("="*80)
print(summary_df.to_string(index=False))

# Average minimum distances by method
print("\n" + "="*80)
print("AVERAGE MINIMUM DISTANCE BY METHOD (lower = more similar)")
print("="*80)
method_avg = summary_df.groupby('Method')['Min_Distance'].mean().sort_values()
for method, avg_dist in method_avg.items():
    print(f"{method:15s}: {avg_dist:.6f}")

# Average unaligned distances by method
print("\n" + "="*80)
print("AVERAGE UNALIGNED DISTANCE BY METHOD")
print("="*80)
method_avg_unaligned = summary_df.groupby('Method')['Unaligned_Distance'].mean().sort_values()
for method, avg_dist in method_avg_unaligned.items():
    print(f"{method:15s}: {avg_dist:.6f}")

================================================================================
GRAPH MATCHING RESULTS SUMMARY
================================================================================
     Method Replicate_Pair  Min_Distance  Unaligned_Distance  Distance_Reduction  Reduction_Percentage(%)
 SpatialCOC   Rep1_vs_Rep2      0.032015            0.705136            0.673121                95.459733
 SpatialCOC   Rep1_vs_Rep3      0.007364            0.328222            0.320858                97.756313
 SpatialCOC   Rep2_vs_Rep3      0.035020            0.697352            0.662332                94.978173
SpatialGlue   Rep1_vs_Rep2      0.063992            0.659802            0.595810                90.301322
SpatialGlue   Rep1_vs_Rep3      0.125532            0.541153            0.415620                76.802825
SpatialGlue   Rep2_vs_Rep3      0.154660            0.543575            0.388914                71.547533
     Seurat   Rep1_vs_Rep2      0.180691            0.599036            0.418345                69.836363
     Seurat   Rep1_vs_Rep3      0.150814            0.220010            0.069196                31.451407
     Seurat   Rep2_vs_Rep3      0.074865            0.452304            0.377439                83.448107
    STAGATE   Rep1_vs_Rep2      0.036836            0.809748            0.772912                95.450930
    STAGATE   Rep1_vs_Rep3      0.056307            0.604051            0.547744                90.678434
    STAGATE   Rep2_vs_Rep3      0.080665            0.341054            0.260389                76.348310

================================================================================
AVERAGE MINIMUM DISTANCE BY METHOD (lower = more similar)
================================================================================
SpatialCOC     : 0.024800
STAGATE        : 0.057936
SpatialGlue    : 0.114728
Seurat         : 0.135457

================================================================================
AVERAGE UNALIGNED DISTANCE BY METHOD
================================================================================
Seurat         : 0.423783
SpatialCOC     : 0.576903
SpatialGlue    : 0.581510
STAGATE        : 0.584951
[12]:
pivot_dist = (summary_df
              .pivot(index='Replicate_Pair', columns='Method', values='Min_Distance')
              .loc[['Rep1_vs_Rep2', 'Rep1_vs_Rep3', 'Rep2_vs_Rep3'],
                   ['SpatialCOC', 'SpatialGlue', 'Seurat', 'STAGATE']])

pivot_sim = 1 - pivot_dist
print(pivot_sim)

output_file = 'Cosine_Similarity.xlsx'
with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
    pivot_sim.to_excel(writer, sheet_name='Cosine_Similarity', index=True)
Method          SpatialCOC  SpatialGlue    Seurat   STAGATE
Replicate_Pair
Rep1_vs_Rep2      0.967985     0.936008  0.819309  0.963164
Rep1_vs_Rep3      0.992636     0.874468  0.849186  0.943693
Rep2_vs_Rep3      0.964980     0.845340  0.925135  0.919335

Boxplot: Graph Distance Distribution Across Methods

[13]:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

# ---------- 1. Load and prepare data ----------
df = pd.read_excel('Cosine_Similarity.xlsx')
methods_disp = ['SpatialCOC', 'SpatialGlue', 'Seurat', 'STAGATE']
colors       = ['#D0836F', '#E4BE64', '#94aebd', '#8D8E99']
plot_data    = [df[col].dropna().values for col in methods_disp]

# ---------- 2. Configure plot style ----------
fontsize = 16
plt.rcParams['font.sans-serif'] = ['Arial']
plt.rcParams['font.size'] = fontsize
plt.rcParams['axes.edgecolor'] = 'black'

# ---------- 3. Set y-axis limits ----------
all_vals = np.concatenate(plot_data)
delta = 0.05 * (all_vals.max() - all_vals.min())
y_min, y_max = all_vals.min() - delta, all_vals.max() + delta

# ---------- 4. Create boxplot ----------
fig, ax = plt.subplots(figsize=(8, 5.3))
bp = ax.boxplot(plot_data, patch_artist=True,
                boxprops=dict(linewidth=1, edgecolor='black'),
                medianprops=dict(linewidth=1, color='black'),
                whiskerprops=dict(linewidth=1, color='black'),
                capprops=dict(linewidth=1, color='black'),
                widths=0.55, showfliers=False)

# Fill boxes with method-specific colors
for patch, color in zip(bp['boxes'], colors):
    patch.set_facecolor(color)

# ---------- 5. Configure axes labels and ticks ----------
ax.set_xticks(range(1, len(methods_disp)+1))
ax.set_xticklabels(methods_disp, fontsize=fontsize)
ax.set_xlabel('Methods', fontsize=fontsize)
ax.set_ylabel('Cosine Similarity Across PAGA Graphs', fontsize=fontsize)

# Set y-axis tick font size
ax.tick_params(axis='y', labelsize=fontsize)

ax.set_ylim(y_min, 1)

# Set spine width for all axes
for spine in ax.spines.values():
    spine.set_linewidth(2)

# Add grid lines for better readability
ax.grid(axis='y', color='gray', linestyle='--', alpha=1, zorder=-10, linewidth=1.5)
for x in range(1, len(methods_disp)+1):
    ax.axvline(x=x, color='gray', linestyle='--', alpha=1, zorder=-10, linewidth=1.5)

plt.tight_layout(pad=0.1, rect=[0.03, 0, 1, 0.98])
# plt.savefig('Cosine_Similarity_Boxplot.png', dpi=500)
# plt.savefig('Cosine_Similarity_Boxplot.eps')
plt.show()
../../_images/Reproduction_Mouse_Thymus_Datasets_7_Trajectory_Inference_23_0.png