Moran Visualization of the Mouse Spleen Dataset

Loading Packages

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

import scanpy as sc
import squidpy as sq
import uuid
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

import sys
sys.path.append('../Mouse_Spleen/')

Loading Data

[2]:
slices = ['1', '2']  ## 1, 2
# List of methods
methods = ['SpatialCOC', 'COSMOS', 'SpatialGlue', 'Seurat', 'MultiVI', 'MultiMAP', 'STAGATE', 'SpaGCN', 'Modality1', 'Modality2']
# Create a dictionary to store data for each slice
adata_results = {}

# Load data and store it
for slice_name in slices:
    path = f'../../Mouse_Spleen_Replicate{slice_name}.h5ad'
    result = sc.read_h5ad(path)  # Assuming 'sc' is the Scanpy library
    adata_results[slice_name] = result
[3]:
adata_results
[3]:
{'1': AnnData object with n_obs × n_vars = 2568 × 0
     obs: 'SpatialCOC', 'SpatialGlue', 'STAGATE', 'SpaGCN', 'Modality1', 'Modality2', 'MultiMAP', 'MultiVI', 'Seurat', 'COSMOS'
     uns: 'COSMOS_colors', 'MultiMAP_colors', 'Seurat_colors'
     obsm: 'COSMOS', 'Modality1', 'Modality2', 'MultiMAP', 'MultiVI', 'STAGATE', 'Seurat', 'SpatialCOC', 'SpatialGlue', 'spatial',
 '2': AnnData object with n_obs × n_vars = 2768 × 0
     obs: 'SpatialCOC', 'SpatialGlue', 'STAGATE', 'SpaGCN', 'Modality1', 'Modality2', 'MultiMAP', 'MultiVI', 'Seurat', 'COSMOS'
     uns: 'COSMOS_colors', 'Seurat_colors', 'SpaKnit_colors'
     obsm: 'COSMOS', 'Modality1', 'Modality2', 'MultiMAP', 'MultiVI', 'STAGATE', 'Seurat', 'SpatialCOC', 'SpatialGlue', 'spatial'}

Calculating Moran’s I Score

[4]:
# Dictionary to store Moran's I scores for each slice
moran_scores = {}

# Iterate over each slice and its corresponding data
for slice_name, adata in adata_results.items():
    print(slice_name)
    # Construct the spatial graph (adjust parameters based on the data type)
    sq.gr.spatial_neighbors(adata, coord_type='grid')  # or 'generic'
    moran_scores[slice_name] = {}

    # Iterate over each method
    for method in methods:
        # Skip if the method is not present in the observation columns
        if method not in adata.obs.columns:
            continue

        # Get the cluster information for the current method
        cluster = adata.obs[method]
        # Get unique categories, ignoring NaN values
        categories = cluster.dropna().unique()
        moran_scores[slice_name][method] = {}

        # Iterate over each category
        for category in categories:
            # Create a mask for the current category
            mask = cluster == category
            # Generate a unique temporary column name
            temp_col = f"temp_{uuid.uuid4().hex}"

            try:
                # Add the mask as a new column in the observation DataFrame
                adata.obs[temp_col] = mask.astype(int)
                # Calculate Moran's I spatial autocorrelation
                sq.gr.spatial_autocorr(
                    adata,
                    mode="moran",
                    n_perms=100,  # Number of permutations for significance testing
                    attr="obs",  # Attribute type (observations)
                    genes=[temp_col],  # Use the temporary column name
                    n_jobs=4,  # Number of jobs to run in parallel
                    seed=2024  # Random seed for reproducibility
                )
                # Retrieve the Moran's I score from the results
                score = adata.uns['moranI'].loc[temp_col, 'I']
                # Store the score in the dictionary
                moran_scores[slice_name][method][category] = score
            finally:
                # Clean up: Remove the temporary column
                if temp_col in adata.obs:
                    del adata.obs[temp_col]
1
2

Storing the Results as an Excel

[5]:
# Function to convert category keys to integers and sort them in ascending order
def convert_and_sort_categories(moran_scores):
    for slice_name, methods in moran_scores.items():
        for method, categories in methods.items():
            # Convert category keys to integers and sort them
            sorted_categories = {
                int(k): v for k, v in sorted(categories.items(), key=lambda x: int(x[0]))
            }
            moran_scores[slice_name][method] = sorted_categories
    return moran_scores

# Convert category keys to integers and sort them
moran_scores = convert_and_sort_categories(moran_scores)

# Create an Excel file
with pd.ExcelWriter("moran_scores_output.xlsx") as writer:
    # Dictionary to store the average values for all slices
    all_averages = {}

    for slice_name, methods in moran_scores.items():
        # Convert to DataFrame (each row represents a method, column names are categories)
        df = pd.DataFrame.from_dict(methods, orient="index")

        # Calculate the mean value for each row
        averages = df.mean(axis=1)
        all_averages[slice_name] = averages

        # Clean the sheet name (Excel limit: max 31 characters, no special symbols)
        sheet_name = str(slice_name)\
            .replace(":", "_").replace("/", "_")[:31]

        # Write to Excel (missing values are represented as empty strings)
        df.to_excel(
            writer,
            sheet_name=sheet_name,
            na_rep="",
            index_label="Method"
        )

    # Store the average values of all slices in a new sheet
    averages_df = pd.DataFrame.from_dict(all_averages, orient="index")

    # Transpose the averages DataFrame so that each row corresponds to a slice and each column corresponds to a method's average value
    averages_df = averages_df.transpose()

    # Write the averages sheet
    averages_df.to_excel(
        writer,
        sheet_name="Averages",
        index_label="Method"
    )
[ ]:
# Define methods
methods = ['SpatialCOC', 'COSMOS', 'SpatialGlue', 'Seurat', 'MultiVI', 'MultiMAP', 'STAGATE', 'SpaGCN']
# Define colors corresponding to each method
colors = ['#D0836F', '#df871b', '#E4BE64', '#94aebd', '#87AC9A', '#9B8169', '#8D8E99', '#F5ECBA']

# Set y-axis range (adjust according to actual data)
y_min = -0.02
y_max = 1.02

# Set global font size and font
fontsize = 24
plt.rcParams['font.sans-serif'] = ['Arial']
plt.rcParams['font.size'] = fontsize

# Read the Excel file
file_path = 'moran_scores_output.xlsx'  # Replace with your Excel file path
xls = pd.ExcelFile(file_path)

# Get all sheet names (each sheet represents a slice)
slices = xls.sheet_names

# Iterate through each slice
for slice_name in slices:
    # Read data from the current sheet
    df = pd.read_excel(xls, sheet_name=slice_name)

    # Prepare data: Moran score for each method
    data = []
    for method in methods:
        # Get the data row for the current method and ignore missing values
        method_data = df.loc[df['Method'] == method].drop(columns=['Method']).values.flatten()
        method_data = method_data[~np.isnan(method_data)]  # Ignore NaN values
        data.append(method_data)

    # If all data for a method is missing, skip that method
    data = [d for d in data if len(d) > 0]

    # If there is no valid data for any method, skip the current slice
    if not data:
        print(f"No valid data for {slice_name}, skipping...")
        continue

    # Calculate the medians of Modality1 and Modality2 separately
    modality1_data = df.loc[df['Method'] == 'Modality1'].drop(columns=['Method']).values.flatten()
    modality1_data = modality1_data[~np.isnan(modality1_data)]  # Ignore NaN values
    median_modality1 = np.median(modality1_data) if len(modality1_data) > 0 else None

    modality2_data = df.loc[df['Method'] == 'Modality2'].drop(columns=['Method']).values.flatten()
    modality2_data = modality2_data[~np.isnan(modality2_data)]  # Ignore NaN values
    median_modality2 = np.median(modality2_data) if len(modality2_data) > 0 else None

    # Determine the best performance between the two medians
    if median_modality1 is not None and median_modality2 is not None:
        best_median = max(median_modality1, median_modality2)  # Choose the smaller median as the best performance
    else:
        best_median = None

    # Create a new figure
    plt.figure(figsize=(5, 5))

    # Draw the boxplot
    bp = plt.boxplot(data, patch_artist=True,
                     boxprops=dict(linestyle='-', linewidth=1, edgecolor='black'),  # Box edges
                     medianprops=dict(linestyle='-', linewidth=1, color='black'),  # Median line
                     whiskerprops=dict(linestyle='-', linewidth=1, color='black'),  # Whiskers
                     capprops=dict(linestyle='-', linewidth=1, color='black'),  # Caps
                     widths=0.85,  # Box width
                     showfliers=False)  # Do not show outliers

    # Color the boxes
    for patch, color in zip(bp['boxes'], colors[:len(data)]):  # Color only the boxes with actual data
        patch.set_facecolor(color)

    # Remove x-axis tick labels
    plt.xticks([])  # Remove x-axis tick labels

    # Set a unified y-axis range
    plt.ylim(y_min, y_max)

    # Add y-axis label
    plt.ylabel('Moran\'s I Score', fontsize=fontsize)

    # Thicken the figure's border
    for spine in plt.gca().spines.values():
        spine.set_linewidth(2)

    # Add grid lines
    plt.grid(axis='y', color='gray', linestyle='--', alpha=1, zorder=-10, linewidth=1.5)

    # Add vertical dashed lines
    for x in range(1, len(data) + 1):
        plt.axvline(x=x, color='gray', linestyle='--', alpha=1, zorder=-10, linewidth=1.5)

    # Add a red dashed line for the median of single modality values
    if best_median is not None:
        plt.axhline(y=best_median, color='#FF0000', linestyle='--', linewidth=2.5, zorder=10)

    # Print plot information
    print(f"Drawing plot for replicate {slice_name} - Moran Score")

    # Adjust layout
    plt.tight_layout(pad=0.1, rect=[0, 0, 1, 1])

    # Save the image
    if slice_name == "Averages":
        save_path = "./"
    else:
        save_path = f"./replicate{slice_name}/"
    plt.savefig(save_path + f"Moran_Score.png", dpi=500)
    plt.savefig(save_path + f"Moran_Score.eps")

    plt.show()
Drawing plot for replicate 1 - Moran Score
../../_images/Reproduction_Mouse_Spleen_Datasets_5_Moran_10_1.png
Drawing plot for replicate 2 - Moran Score
../../_images/Reproduction_Mouse_Spleen_Datasets_5_Moran_10_3.png
Drawing plot for replicate Averages - Moran Score
../../_images/Reproduction_Mouse_Spleen_Datasets_5_Moran_10_5.png