← Back to list

Journal Figure Replication | Plotting a Composite Correlation Network Heatmap with Python

Composite Correlation Network Heatmap

ZHEMING XU in Top Python Libraries · 2026-06-11 12:45 · 7 claps · 18.8 min read paywalled
#python #data-science #data-visualization #machine-learning #artificial-intelligence
Open on Medium ↗
Wiki topics: ML · Machine Learning AI · AI · General VIS · Visual & Graphic Design EDU · Education & Learning 🔬 · Science · General

Journal Figure Replication | Plotting a Composite Correlation Network Heatmap with Python

Result

Paper: Do ecosystem service gains promote human Well-being? Unpacking the ecosystem–human well-being link in fragile landscapes

Imitation:

Code Explanation

  1. Library imports and font configuration
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.colorbar as colorbar
from matplotlib.lines import Line2D
import matplotlib.colors as mcolors
import matplotlib
import pandas as pd
from scipy.stats import pearsonr
import os
matplotlib.rcParams['pdf.fonttype'] = 42
matplotlib.rcParams['ps.fonttype'] = 42
plt.rcParams['font.family'] = 'serif'
plt.rcParams['font.serif'] = ['Times New Roman']
plt.rcParams['axes.unicode_minus'] = False
  1. Color library configuration and color palette selection
color_library = {
    1: {
        'heatmap_negative': "#4575b4", 'heatmap_zero': "#ffffbf", 'heatmap_positive': "#d73027",
        'center_circle_face': "#4d4d4d", 'center_text': "#ffffff",
    },
}

COLOR_CHOICE = 5  # Select the color scheme configuration
selected_colors = color_library[COLOR_CHOICE]  # Get the chosen color configuration
def get_cmap_from_selection(colors):
    nodes = [0.0, 0.5, 1.0]  # Define the node positions for the color gradient: 0 for negative values, 0.5 for the midpoint, and 1 for positive values
    colors_list = [colors['heatmap_negative'], colors['heatmap_zero'], colors['heatmap_positive']]  # Extract color configurations to build a list
    cmap = mcolors.LinearSegmentedColormap.from_list("custom_corr_cmap", list(zip(nodes, colors_list)))  # Generate the custom Colormap
    return cmap  # Return the generated colormap object
current_cmap = get_cmap_from_selection(selected_colors)  # Generate the colormap based on the currently selected color scheme
norm = mcolors.Normalize(vmin=-1, vmax=1)  # Set the normalization range for the colormap
  1. Data analysis functions

Including feature data extraction, target data extraction, calculation of correlation and significance between features, calculation of correlation and significance between data and targets for each period, and saving of analysis results.

def analyze_raw_data(raw_data_path, output_analysis_path, years, vars_list):
    calculated_data = {}  # Used to store the calculation results
    with pd.ExcelWriter(output_analysis_path) as writer:  # Use pandas to create an Excel writer object, preparing to save the file
        for year in years:  # Iterate through each year
            df_all = pd.read_excel(raw_data_path, sheet_name=f'{year}_RawData', index_col=0)  # Read the Sheet corresponding to the current year
            df_vars = df_all[vars_list]  # Feature data
            center_series = df_all['SHDI']  # Target data

            # Perform correlation analysis between feature data
            corr_matrix = df_vars.corr(method='pearson')
            # Used to store P-values
            p_values = pd.DataFrame(index=vars_list, columns=vars_list, dtype=float)

            for c1 in vars_list:  # Rows
                for c2 in vars_list:  # Columns
                    if c1 == c2:  # If it is the same variable
                        p_values.loc[c1, c2] = 0.0
                    else:
                        _, p = pearsonr(df_vars[c1], df_vars[c2])  # Calculate the correlation coefficient and P-value for the two columns of data
                        p_values.loc[c1, c2] = p  # Fill the calculated P-value into the corresponding position in the matrix

            center_corrs = []  # Used to store the correlation between features and the target
            for var in vars_list:  # Iterate through each variable
                r, _ = pearsonr(df_vars[var], center_series)  # Calculate the correlation coefficient between the current variable and the target
                center_corrs.append(r)  # Add the correlation coefficient to the list

            # Convert the correlation list into a DataFrame format
            df_center_corr = pd.DataFrame(center_corrs,
                                          index=vars_list,
                                          columns=['Correlation_with_Center'])

            # Save the analysis results
            corr_matrix.to_excel(writer, sheet_name=f'{year}_Corr')  # Correlation matrix
            p_values.to_excel(writer, sheet_name=f'{year}_P_Value')  # P-values
            df_center_corr.to_excel(writer, sheet_name=f'{year}_Center_Corr')  # Center correlation data

            # Store the calculation results directly into the dictionary
            calculated_data[year] = {
                    'corr': corr_matrix.values,
                    'p': p_values.values,
                    'r': df_center_corr['Correlation_with_Center'].values
            }
        return calculated_data  # Directly return the calculated data for subsequent use
  1. Network edge styling function

It sets the thickness and color of the network lines based on the analysis results.

def get_line_style(r_value):
    abs_r = abs(r_value)  # Calculate the absolute value of the correlation coefficient
    c_pos = selected_colors['heatmap_positive']  # Color used for positive correlation
    c_neg = selected_colors['heatmap_negative']  # Color used for negative correlation

    # Determine the color based on whether the r-value is positive or negative
    if r_value >= 0:
        line_color = c_pos
    else:
        line_color = c_neg

    # Determine linewidth, linestyle, and alpha transparency based on the absolute value threshold
    if abs_r < 0.10:
        return {'color': line_color, 'linestyle': '--', 'linewidth': 1.0, 'alpha': 0.5}
    elif 0.10 <= abs_r < 0.25:
        return {'color': line_color, 'linestyle': '-', 'linewidth': 1.5, 'alpha': 0.65}
    elif 0.25 <= abs_r < 0.50:
        return {'color': line_color, 'linestyle': '-', 'linewidth': 3.0, 'alpha': 0.8}
    else:
        return {'color': line_color, 'linestyle': '-', 'linewidth': 5.0, 'alpha': 1.0}
  1. Heatmap plotting function

It generates three triangular heatmaps for different ecoregions and adds anchor points for the network edges.

def draw_triangle_heatmap(ax, corr_mat, p_mat, variables, start_x, start_y, type='bottom-left', title_year=''):
    n = len(variables)  # Number of variables
    connection_points = []  # Used to store the anchor coordinates for the network lines

    # Left Triangle / Bottom-Left layout branch
    for i in range(n):  # Iterate through each row
        cols_to_draw = n - i  # Calculate the number of columns to draw for the current row (decreases row by row)
        for j_visual in range(cols_to_draw):  # Iterate through each column of the current row
            col_data_idx = n - 1 - j_visual  # Map the data column index (reverse order)
            row_data_idx = i  # Set the data row index
            val = corr_mat[row_data_idx, col_data_idx]  # Get the corresponding value from the correlation matrix
            p = p_mat[row_data_idx, col_data_idx]  # Get the corresponding value from the P-value matrix
            rect_x = start_x + j_visual  # Calculate the X-axis coordinate for the cell block
            rect_y = start_y - i  # Calculate the Y-axis coordinate for the cell block

            # Draw the cell block
            rect = patches.Rectangle((rect_x, rect_y),
                                     1,
                                     1,
                                     facecolor=current_cmap(norm(val)),
                                     edgecolor='white')
            ax.add_patch(rect)  # Add the rectangle patch to the axes

            # Label text configuration
            text_color = 'white' if abs(val) > 0.6 else 'black'  # Determine text color based on the darkness of the background
            # Plot the correlation coefficient values
            ax.text(rect_x + 0.5,
                    rect_y + 0.35,
                    f"{val:.2f}",
                    ha='center',
                    va='center',
                    fontsize=15,
                    color=text_color,
                    fontweight='normal')

            # Set significance symbols
            if p < 0.05:
                mark = '***' if p < 0.001 else ('**' if p < 0.01 else '*')  # Determine the number of asterisks based on the P-value threshold
                # Plot the significance asterisks
                ax.text(rect_x + 0.5,
                        rect_y + 0.52,
                        mark, ha='center',
                        va='center',
                        fontsize=15,
                        color=text_color,
                        fontweight='bold')

    # Left Y-axis labels
    for i in range(n):  # Iterate through rows to draw Y-axis labels
        label_y = start_y - i + 0.5  # Y-coordinate for the label
        # Plot the variable names on the left side
        ax.text(start_x - 0.2,
                label_y,
                variables[i],
                ha='right',
                va='center',
                fontsize=12,
                fontweight='bold')

    # Top labels for the X-axis
    top_labels = variables[::-1]  # Reverse the variable list for the top X-axis labels
    for i in range(n):  # Iterate through columns
        label_x = start_x + i + 0.5  # X-coordinate for the label
        # Plot the variable labels at the top
        ax.text(label_x,
                start_y + 1.2,
                top_labels[i],
                ha='center',
                va='bottom',
                fontsize=12,
                fontweight='bold')

    # Year annotation
    ax.text(start_x,
            start_y + 1.2,
            title_year,
            ha='right',
            va='center',
            fontsize=14,
            fontweight='bold')

    # Iterate through rows for alternative layout branch
    for i in range(n):
        cols_count = n - i  # Calculate the number of cell blocks for the current row
        offset = i  # Calculate the horizontal offset for each row
        for j_visual in range(cols_count):  # Iterate through columns
            rect_x = start_x + offset + j_visual  # X-axis coordinate for the cell block
            rect_y = start_y - i  # Y-axis coordinate for the cell block
            row_data_idx = i  # Data row index
            col_data_idx = i + j_visual  # Data column index
            val = corr_mat[row_data_idx, col_data_idx]  # Get the correlation coefficient value
            p = p_mat[row_data_idx, col_data_idx]  # Get the P-value

            # Create the cell block
            rect = patches.Rectangle((rect_x, rect_y),
                                     1,
                                     1,
                                     facecolor=current_cmap(norm(val)),
                                     edgecolor='white')
            ax.add_patch(rect)  # Add patch
            text_color = 'white' if abs(val) > 0.6 else 'black'  # Set text color

            # Plot values
            ax.text(rect_x + 0.5,
                    rect_y + 0.35,
                    f"{val:.2f}",
                    ha='center',
                    va='center',
                    fontsize=15,
                    color=text_color,
                    fontweight='normal')
            if p < 0.05:  # Determine significance
                mark = '***' if p < 0.001 else ('**' if p < 0.01 else '*')
                # Plot significance marks
                ax.text(rect_x + 0.5,
                        rect_y + 0.52,
                        mark,
                        ha='center',
                        va='center',
                        fontsize=15,
                        color=text_color,
                        fontweight='bold')

    # Right Y-axis labels
    for i in range(n):  # Iterate through rows
        label_y = start_y - i + 0.5  # Y-coordinate for the label
        # Plot right side labels
        ax.text(start_x + n + 0.2,
                label_y,
                variables[i],
                ha='left',
                va='center',
                fontsize=12,
                fontweight='bold')

    # Top labels for the X-axis (Alternative branch layout)
    for i in range(n):  # Iterate through columns
        label_x = start_x + i + 0.5  # X-coordinate
        # Plot variable names at the top
        ax.text(label_x,
                start_y + 1.2,
                variables[i],
                ha='center',
                va='bottom',
                fontsize=12,
                fontweight='bold')

    # Year label
    ax.text(start_x + n,
            start_y + 1.2,
            title_year,
            ha='left',
            va='center',
            fontsize=14,
            fontweight='bold')

    # Lower triangle layout loop configuration
    for i in range(n):  # Rows
        for j in range(i + 1):  # Columns
            rect_x = start_x + j  # X-coordinate for the cell block
            rect_y = start_y - i  # Y-coordinate for the cell block
            val = corr_mat[i, j]  # Correlation coefficient value
            p = p_mat[i, j]  # Get the P-value

            # Create the cell block
            rect = patches.Rectangle((rect_x, rect_y),
                                     1,
                                     1,
                                     facecolor=current_cmap(norm(val)),
                                     edgecolor='white')
            ax.add_patch(rect)  # Add patch

            text_color = 'white' if abs(val) > 0.6 else 'black'  # Text color
            # Plot correlation coefficients
            ax.text(rect_x + 0.5,
                    rect_y + 0.35,
                    f"{val:.2f}",
                    ha='center',
                    va='center',
                    fontsize=15,
                    color=text_color, 
                    fontweight='normal')
            if p < 0.05:  # Significance
                mark = '***' if p < 0.001 else ('**' if p < 0.01 else '*')
                ax.text(rect_x + 0.5,
                        rect_y + 0.52,
                        mark,
                        ha='center',
                        va='center',
                        fontsize=15,
                        color=text_color,
                        fontweight='bold')

    # Left Y-axis labels (Alternative lower triangle section)
    for i in range(n):  # Rows
        label_y = start_y - i + 0.5  # Y-coordinate
        # Plot labels
        ax.text(start_x - 0.2,
                label_y,
                variables[i],
                ha='right',
                va='center',
                fontsize=12,
                fontweight='bold')
        ax.text(label_x,
                start_y - (n - 1) - 0.5,
                variables[i],
                ha='center',
                va='top',
                fontsize=12,
                fontweight='bold')

    # Plot Year text label
    ax.text(start_x,
            start_y - (n - 1) - 0.4,
            title_year,
            ha='right',
            va='center',
            fontsize=14,
            fontweight='bold')

    return connection_points
  1. Main plotting function

Including canvas setup, position layout for the three heatmaps, heatmap function execution, drawing of the central target circular pattern, generation of network edges, addition of colorbars and legends, and output save path configuration.

def create_complex_layout_plot(data, vars_list):
    fig, ax = plt.subplots(figsize=(20, 16))  # Create the canvas and coordinate axes
    ax.set_aspect('equal')  # Force equal aspect ratio to ensure cell blocks remain perfectly square
    n = len(vars_list)  # Number of variables
    gap_width = 2.0  # Set the middle gap width spacing

    start_x_2000 = -gap_width / 2 - n  # Starting X-coordinate for the top-left heatmap
    start_y_2000 = n + 2  # Starting Y-coordinate for the top-left heatmap

    start_x_2010 = -gap_width / 2 - n  # Starting X-coordinate for the bottom-right heatmap
    start_y_2010 = 0  # Starting Y-coordinate for the bottom-right heatmap

    start_x_2020 = gap_width / 2  # Starting X-coordinate for the top-right heatmap
    start_y_2020 = start_y_2000  # Starting Y-coordinate for the top-right heatmap

    # Plot the three triangular heatmaps
    pts_2000 = draw_triangle_heatmap(ax,
                                     data[2000]['corr'],
                                     data[2000]['p'],
                                     vars_list,
                                     start_x=start_x_2000,
                                     start_y=start_y_2000,
                                     type='top-left',
                                     title_year='2000')

    pts_2020 = draw_triangle_heatmap(ax,
                                     data[2020]['corr'],
                                     data[2020]['p'],
                                     vars_list,
                                     start_x=start_x_2020,
                                     start_y=start_y_2020,
                                     type='top-right',
                                     title_year='2020')

    pts_2010 = draw_triangle_heatmap(ax,
                                     data[2010]['corr'],
                                     data[2010]['p'],
                                     vars_list,
                                     start_x=start_x_2010,
                                     start_y=start_y_2010,
                                     type='bottom-left',
                                     title_year='2010')

    # Plot the central node
    center_y = ((start_y_2000 - n) + (start_y_2010 + 1)) / 2  # Y-coordinate for the central circle
    center_x = 0  # X-coordinate for the central circle

    c_face = selected_colors['center_circle_face']  # Fill color for the central circle
    c_text = selected_colors['center_text']  # Text color for the central circle

    # Draw the central large circle target
    ax.scatter(center_x,
               center_y,
               s=6000,
               marker='o',
               facecolor=c_face,
               edgecolor='none',
               zorder=100)

    # Plot the center node labels
    # Plot the first line of text inside the circle
    ax.text(center_x,
            center_y + 0.25,
            "SHDI",
            ha='center',
            va='bottom',
            fontsize=15,
            fontweight='bold',
            color=c_text,
            zorder=101)
    # Plot the second line of text inside the circle
    ax.text(center_x,
            center_y,
            "Pastoral area",
            ha='center',
            va='top',
            fontsize=13,
            fontweight='bold',
            color=c_text,
            zorder=101)

    ax.set_xlim(start_x_2000 - 3, start_x_2020 + n + 3)  # X-axis display limits
    ax.set_ylim(start_y_2010 - n - 2, start_y_2000 + 3)  # Y-axis display limits
    ax.axis('off')  # Hide borders, frames, and ticks

    # Colorbar configuration
    cbar_ax = fig.add_axes([0.7, 0.25, 0.013, 0.25])  # Add a new axes container into the figure layout for the colorbar
    # Create the colorbar object
    cb = colorbar.ColorbarBase(cbar_ax,
                               cmap=current_cmap,
                               norm=norm,
                               orientation='vertical')
    cb.set_label("Pearson's r", size=18)  # Set colorbar title label
    cb.set_ticks([-1, -0.5, 0, 0.5, 1])  # Colorbar tick positions
    cb.outline.set_visible(False)  # Remove colorbar outer border frame
    cb.ax.tick_params(size=0, labelsize=18)  # Configure tick parameters (hide marks, set label size)

    # Legend configuration
    c_pos = selected_colors['heatmap_positive']  # Positive correlation color
    c_neg = selected_colors['heatmap_negative']  # Negative correlation color

    # Define legend element list, separated into two correlation groups
    legend_elements = [
        # Positive correlation group
        Line2D([0], [0], color=c_pos, lw=1.0, linestyle='--', alpha=0.5, label='Positive < 0.10'),
        Line2D([0], [0], color=c_pos, lw=1.5, linestyle='-', alpha=0.65, label='Positive 0.10 - 0.25'),
        Line2D([0], [0], color=c_pos, lw=3.0, linestyle='-', alpha=0.8, label='Positive 0.25 - 0.50'),
        Line2D([0], [0], color=c_pos, lw=5.0, linestyle='-', alpha=1.0, label='Positive > 0.50'),
        # Negative correlation group
        Line2D([0], [0], color=c_neg, lw=1.0, linestyle='--', alpha=0.5, label='Negative > -0.10'),
        Line2D([0], [0], color=c_neg, lw=1.5, linestyle='-', alpha=0.65, label='Negative -0.10 to -0.25'),
        Line2D([0], [0], color=c_neg, lw=3.0, linestyle='-', alpha=0.8, label='Negative -0.25 to -0.50'),
        Line2D([0], [0], color=c_neg, lw=5.0, linestyle='-', alpha=1.0, label='Negative < -0.50')
    ]

    # Plot figure legend
    ax.legend(handles=legend_elements,
              loc='lower right',
              bbox_to_anchor=(0.76, 0.2),
              title="Correlation Network (Lines)",
              frameon=False,
              fontsize=14,
              title_fontsize=16,
              ncol=1)
    # Figure panel sub-label annotation
    ax.text(start_x_2020 + n - 4, start_y_2010 - n + 1, "(a)", fontsize=24, fontweight='bold')
  1. Execution block, including defining features to analyze, sheets, input/output file save paths, data analysis execution, and plotting function calls.
if __name__ == "__main__":
    vars_list = ['CS', 'FP', 'HQ', 'SR', 'WY']  # Features
    years = [2000, 2010, 2020]  # Sheets
    raw_data_file = r'D:\folder\raw_data.xlsx'  # Full path to the raw data file
    analysis_result_file = r'D:\folder\simulation_results.xlsx'  # Full path to the analysis results file

    # Call the data analysis function
    plot_data = analyze_raw_data(raw_data_file,
                                 analysis_result_file,
                                 years,
                                 vars_list)
    # Call the main plotting function
    create_complex_layout_plot(plot_data,
                               vars_list)

Data

Input data

[embed]Medium_datasets/Composite Correlation Network Heatmap/raw_data.xlsx at main ·… Contribute to checkming00/Medium_datasets development by creating an account on GitHub.github.com

Output data

[embed]Medium_datasets/Composite Correlation Network Heatmap/simulation_results.xlsx at main ·… Contribute to checkming00/Medium_datasets development by creating an account on GitHub.github.com

Complete Code

# =========================================================================================
# ======================================1.Import libraries =========================================
# =========================================================================================
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.colorbar as colorbar
from matplotlib.lines import Line2D
import matplotlib.colors as mcolors
import matplotlib
import pandas as pd
from scipy.stats import pearsonr
matplotlib.rcParams['pdf.fonttype'] = 42
matplotlib.rcParams['ps.fonttype'] = 42
plt.rcParams['font.family'] = 'serif'
plt.rcParams['font.serif'] = ['Times New Roman']
plt.rcParams['axes.unicode_minus'] = False

# =========================================================================================
# =====================================2.Color library =========================
# =========================================================================================
color_library = {
    1: {
        'heatmap_negative': "#4575b4",
        'heatmap_zero': "#ffffbf",
        'heatmap_positive': "#d73027",
        'center_circle_face': "#4d4d4d",
        'center_text': "#ffffff",

    },
    2: {
        'heatmap_negative': "#2166ac", 'heatmap_zero': "#f7f7f7", 'heatmap_positive': "#b2182b",
        'center_circle_face': "#333333", 'center_text': "#ffffff",
    },
    3: {
        'heatmap_negative': "#762a83", 'heatmap_zero': "#f7f7f7", 'heatmap_positive': "#1b7837",
        'center_circle_face': "#40004b", 'center_text': "#ffffff",
    },
    4: {
        'heatmap_negative': "#8c510a", 'heatmap_zero': "#f5f5f5", 'heatmap_positive': "#01665e",
        'center_circle_face': "#35978f", 'center_text': "#ffffff",
    },
    5: {
        'heatmap_negative': "#c51b7d", 'heatmap_zero': "#fde0ef", 'heatmap_positive': "#4d9221",
        'center_circle_face': "#276419", 'center_text': "#ffffff",
    },
    6: {
        'heatmap_negative': "#542788", 'heatmap_zero': "#f7f7f7", 'heatmap_positive': "#b35806",
        'center_circle_face': "#e08214", 'center_text': "#ffffff",
    },
    7: {
        'heatmap_negative': "#4d4d4d", 'heatmap_zero': "#e0e0e0", 'heatmap_positive': "#ca0020",
        'center_circle_face': "#000000", 'center_text': "#ffffff",
    },
    8: {
        'heatmap_negative': "#d73027", 'heatmap_zero': "#ffffbf", 'heatmap_positive': "#1a9850",
        'center_circle_face': "#006837", 'center_text': "#ffffff",
    },
    9: {
        'heatmap_negative': "#6b7b8c", 'heatmap_zero': "#f2eee5", 'heatmap_positive': "#b05b53",
        'center_circle_face': "#4a5866", 'center_text': "#ffffff",
    },
    10: {
        'heatmap_negative': "#08519c", 'heatmap_zero': "#f7fbff", 'heatmap_positive': "#006d2c",
        'center_circle_face': "#00441b", 'center_text': "#ffffff",
    },
    11: {
        'heatmap_negative': "#2d004b", 'heatmap_zero': "#1a1a1a", 'heatmap_positive': "#ff00cc",
        'center_circle_face': "#ffffff", 'center_text': "#000000",
    },
    12: {
        'heatmap_negative': "#7f7f7f", 'heatmap_zero': "#ffffff", 'heatmap_positive': "#08519c",
        'center_circle_face': "#252525", 'center_text': "#ffffff",
    },
    13: {
        'heatmap_negative': "#313695", 'heatmap_zero': "#e0f3f8", 'heatmap_positive': "#a50026",
        'center_circle_face': "#74add1", 'center_text': "#000000",
    },
    14: {
        'heatmap_negative': "#2c7fb8", 'heatmap_zero': "#ffffd9", 'heatmap_positive': "#41b6c4",
        'center_circle_face': "#253494", 'center_text': "#ffffff",
    },
    15: {
        'heatmap_negative': "#762a83", 'heatmap_zero': "#ffffff", 'heatmap_positive': "#e08214",
        'center_circle_face': "#542788", 'center_text': "#ffffff",
    },
    16: {
        'heatmap_negative': "#2b83ba", 'heatmap_zero': "#ffffbf", 'heatmap_positive': "#d7191c",
        'center_circle_face': "#2c7bb6", 'center_text': "#ffffff",
    },
    17: {
        'heatmap_negative': "#cccccc", 'heatmap_zero': "#ffffff", 'heatmap_positive': "#000000",
        'center_circle_face': "#666666", 'center_text': "#ffffff",
    },
    18: {
        'heatmap_negative': "#8c510a", 'heatmap_zero': "#f6e8c3", 'heatmap_positive': "#01665e",
        'center_circle_face': "#35978f", 'center_text': "#ffffff",
    },
    19: {
        'heatmap_negative': "#018571", 'heatmap_zero': "#f5f5f5", 'heatmap_positive': "#a6611a",
        'center_circle_face': "#80cdc1", 'center_text': "#000000",
    },
    20: {
        'heatmap_negative': "#8dd3c7", 'heatmap_zero': "#ffffb3", 'heatmap_positive': "#fb8072",
        'center_circle_face': "#80b1d3", 'center_text': "#ffffff",
    }
}

COLOR_CHOICE = 5  # Select the color scheme configuration
selected_colors = color_library[COLOR_CHOICE]  # Get the chosen color configuration
def get_cmap_from_selection(colors):
    nodes = [0.0, 0.5, 1.0]  # Define the node positions for the color gradient: 0 for negative values, 0.5 for the midpoint, and 1 for positive values
    colors_list = [colors['heatmap_negative'], colors['heatmap_zero'], colors['heatmap_positive']]  # Extract color configurations to build a list
    cmap = mcolors.LinearSegmentedColormap.from_list("custom_corr_cmap", list(zip(nodes, colors_list)))  # Generate the custom Colormap
    return cmap  # Return the generated colormap object
current_cmap = get_cmap_from_selection(selected_colors)  # Generate the colormap based on the currently selected color scheme
norm = mcolors.Normalize(vmin=-1, vmax=1)  # Set the normalization range for the colormap

# =========================================================================================
# ======================================3.Data analysis function ==============================
# =========================================================================================
def analyze_raw_data(raw_data_path, output_analysis_path, years, vars_list):
    calculated_data = {}  # Used to store the calculation results
    with pd.ExcelWriter(output_analysis_path) as writer:  # Use pandas to create an Excel writer object, preparing to save the file
        for year in years:  # Iterate through each year
            df_all = pd.read_excel(raw_data_path, sheet_name=f'{year}_RawData', index_col=0)  # Read the sheet corresponding to the current year
            df_vars = df_all[vars_list]  # Feature data
            center_series = df_all['SHDI']  # Target data

            # Perform correlation analysis between feature data
            corr_matrix = df_vars.corr(method='pearson')
            # Used to store P-values
            p_values = pd.DataFrame(index=vars_list, columns=vars_list, dtype=float)

            for c1 in vars_list:  # Rows
                for c2 in vars_list:  # Columns
                    if c1 == c2:  # If it is the same variable
                        p_values.loc[c1, c2] = 0.0
                    else:
                        _, p = pearsonr(df_vars[c1], df_vars[c2])  # Calculate the correlation coefficient and P-value for the two columns of data
                        p_values.loc[c1, c2] = p  # Fill the calculated P-value into the corresponding position in the matrix

            center_corrs = []  # Used to store the correlation between features and the target
            for var in vars_list:  # Iterate through each variable
                r, _ = pearsonr(df_vars[var], center_series)  # Calculate the correlation coefficient between the current variable and the target
                center_corrs.append(r)  # Add the correlation coefficient to the list

            # Convert the correlation list into a DataFrame format
            df_center_corr = pd.DataFrame(center_corrs,
                                          index=vars_list,
                                          columns=['Correlation_with_Center'])

            # Save the analysis results
            corr_matrix.to_excel(writer, sheet_name=f'{year}_Corr')  # Correlation matrix
            p_values.to_excel(writer, sheet_name=f'{year}_P_Value')  # P-values
            df_center_corr.to_excel(writer, sheet_name=f'{year}_Center_Corr')  # Center correlation data

            # Store the calculation results directly into the dictionary
            calculated_data[year] = {
                    'corr': corr_matrix.values,
                    'p': p_values.values,
                    'r': df_center_corr['Correlation_with_Center'].values
            }
        return calculated_data  # Directly return the calculated data for subsequent use

# =========================================================================================
# ================================== 4. Network Edge Styling Function ==================================
# =========================================================================================
def get_line_style(r_value):
    abs_r = abs(r_value)  # Calculate the absolute value of the correlation coefficient
    c_pos = selected_colors['heatmap_positive']  # Color used for positive correlation
    c_neg = selected_colors['heatmap_negative']  # Color used for negative correlation

    # Determine the color based on whether the r-value is positive or negative
    if r_value >= 0:
        line_color = c_pos
    else:
        line_color = c_neg

    # Determine linewidth, linestyle, and alpha transparency based on the absolute value threshold
    if abs_r < 0.10:
        return {'color': line_color, 'linestyle': '--', 'linewidth': 1.0, 'alpha': 0.5}
    elif 0.10 <= abs_r < 0.25:
        return {'color': line_color, 'linestyle': '-', 'linewidth': 1.5, 'alpha': 0.65}
    elif 0.25 <= abs_r < 0.50:
        return {'color': line_color, 'linestyle': '-', 'linewidth': 3.0, 'alpha': 0.8}
    else:
        return {'color': line_color, 'linestyle': '-', 'linewidth': 5.0, 'alpha': 1.0}

# =========================================================================================
# ===================================== 5. Heatmap Plotting Function =====================================
# =========================================================================================
def draw_triangle_heatmap(ax, corr_mat, p_mat, variables, start_x, start_y, type='bottom-left', title_year=''):
    n = len(variables)  # Number of variables
    connection_points = []  # Used to store anchor coordinates for the connection lines

    # Left triangle layout variants
    if type == 'top-left':  # Inverted triangle layout positioned at the top-left
        for i in range(n):  # Iterate through each row
            cols_to_draw = n - i  # Calculate columns to draw for the current row (decreases row by row)
            for j_visual in range(cols_to_draw):  # Iterate through each column in the current row
                col_data_idx = n - 1 - j_visual  # Map the data column index in reverse order
                row_data_idx = i  # Set data row index
                val = corr_mat[row_data_idx, col_data_idx]  # Get the corresponding value from the correlation matrix
                p = p_mat[row_data_idx, col_data_idx]  # Get the corresponding value from the P-value matrix
                rect_x = start_x + j_visual  # Calculate the X-axis coordinate for the cell block
                rect_y = start_y - i  # Calculate the Y-axis coordinate for the cell block

                # Draw the cell block
                rect = patches.Rectangle((rect_x, rect_y),
                                         1,
                                         1,
                                         facecolor=current_cmap(norm(val)),
                                         edgecolor='white')
                ax.add_patch(rect)  # Add the rectangle patch to the axes

                # Text annotation
                text_color = 'white' if abs(val) > 0.6 else 'black'  # Determine text color based on background contrast brightness
                # Draw correlation coefficient value text
                ax.text(rect_x + 0.5,
                        rect_y + 0.35,
                        f"{val:.2f}",
                        ha='center',
                        va='center',
                        fontsize=15,
                        color=text_color,
                        fontweight='normal')
                # Add significance markers
                if p < 0.05:
                    mark = '***' if p < 0.001 else ('**' if p < 0.01 else '*')  # Determine star count based on P-value thresholds
                    # Draw significance stars
                    ax.text(rect_x + 0.5,
                            rect_y + 0.52,
                            mark, ha='center',
                            va='center',
                            fontsize=15,
                            color=text_color,
                            fontweight='bold')

        # Left Y-axis labels
        for i in range(n):  # Iterate through rows to draw Y-axis labels
            label_y = start_y - i + 0.5  # Y-coordinate for the label
            # Draw variable name on the left side
            ax.text(start_x - 0.2,
                    label_y,
                    variables[i],
                    ha='right',
                    va='center',
                    fontsize=12,
                    fontweight='bold')
            # Anchor mapping
            conn_x = start_x + (n - 1 - i) + 1  # Calculate connection anchor X-coordinate for this row
            conn_y = start_y - i + 0.5  # Calculate connection anchor Y-coordinate for this row
            connection_points.append((conn_x, conn_y))  # Append anchor coordinates to the tracking list

        # Top X-axis labels
        top_labels = variables[::-1]  # Reverse the variable list for top X-axis alignment
        for i in range(n):  # Iterate through columns
            label_x = start_x + i + 0.5  # X-coordinate for the label
            # Draw variable label on the top axis boundary
            ax.text(label_x,
                    start_y + 1.2,
                    top_labels[i],
                    ha='center',
                    va='bottom',
                    fontsize=12,
                    fontweight='bold')
        # Year annotation label
        ax.text(start_x,
                start_y + 1.2,
                title_year,
                ha='right',
                va='center',
                fontsize=14,
                fontweight='bold')

    # Right triangle layout variants
    elif type == 'top-right':
        # Iterate through rows
        for i in range(n):
            cols_count = n - i  # Calculate cell blocks to draw for the current row
            offset = i  # Calculate horizontal shift offset for each row
            for j_visual in range(cols_count):  # Iterate through columns
                rect_x = start_x + offset + j_visual  # X-axis coordinate for the cell block
                rect_y = start_y - i  # Y-axis coordinate for the cell block
                row_data_idx = i  # Data row index reference
                col_data_idx = i + j_visual  # Data column index reference
                val = corr_mat[row_data_idx, col_data_idx]  # Extract correlation coefficient value
                p = p_mat[row_data_idx, col_data_idx]  # Extract P-value

                # Instantiate cell patch
                rect = patches.Rectangle((rect_x, rect_y),
                                         1,
                                         1,
                                         facecolor=current_cmap(norm(val)),
                                         edgecolor='white')
                ax.add_patch(rect)  # Add patch
                text_color = 'white' if abs(val) > 0.6 else 'black'  # Assign text color contract
                # Draw values
                ax.text(rect_x + 0.5,
                        rect_y + 0.35,
                        f"{val:.2f}",
                        ha='center',
                        va='center',
                        fontsize=15,
                        color=text_color,
                        fontweight='normal')
                if p < 0.05:  # Evaluate significance thresholds
                    mark = '***' if p < 0.001 else ('**' if p < 0.01 else '*')
                    # Draw stars
                    ax.text(rect_x + 0.5,
                            rect_y + 0.52,
                            mark,
                            ha='center',
                            va='center',
                            fontsize=15,
                            color=text_color,
                            fontweight='bold')

        # Right Y-axis labels
        for i in range(n):  # Iterate through rows
            label_y = start_y - i + 0.5  # Label Y-coordinate alignment
            # Draw label
            ax.text(start_x + n + 0.2,
                    label_y,
                    variables[i],
                    ha='left',
                    va='center',
                    fontsize=12,
                    fontweight='bold')

            # Anchor mapping
            conn_x = start_x + i + 0  # X-coordinate
            conn_y = start_y - i + 0.5  # Y-coordinate
            connection_points.append((conn_x, conn_y))  # Append anchor tracking

        # Top X-axis labels
        for i in range(n):  # Iterate through columns
            label_x = start_x + i + 0.5  # X-coordinate
            # Draw top variable label text
            ax.text(label_x,
                    start_y + 1.2,
                    variables[i],
                    ha='center',
                    va='bottom',
                    fontsize=12,
                    fontweight='bold')
        # Year stamp annotation
        ax.text(start_x + n,
                start_y + 1.2,
                title_year,
                ha='left',
                va='center',
                fontsize=14,
                fontweight='bold')

    # Bottom-left corner layout variants
    elif type == 'bottom-left':
        for i in range(n):  # Rows
            for j in range(i + 1):  # Columns
                rect_x = start_x + j  # Cell block X-coordinate
                rect_y = start_y - i  # Cell block Y-coordinate
                val = corr_mat[i, j]  # Correlation magnitude value
                p = p_mat[i, j]  # Extract P-value matrix location

                # Instantiate cell patch
                rect = patches.Rectangle((rect_x, rect_y),
                                         1,
                                         1,
                                         facecolor=current_cmap(norm(val)),
                                         edgecolor='white')
                ax.add_patch(rect)  # Add patch

                text_color = 'white' if abs(val) > 0.6 else 'black'  # Font contrast assignment
                # Draw core value text strings
                ax.text(rect_x + 0.5,
                        rect_y + 0.35,
                        f"{val:.2f}",
                        ha='center',
                        va='center',
                        fontsize=15,
                        color=text_color, 
                        fontweight='normal')
                if p < 0.05:  # Significance evaluation
                    mark = '***' if p < 0.001 else ('**' if p < 0.01 else '*')
                    ax.text(rect_x + 0.5,
                            rect_y + 0.52,
                            mark,
                            ha='center',
                            va='center',
                            fontsize=15,
                            color=text_color,
                            fontweight='bold')

        # Left Y-axis labels
        for i in range(n):  # Rows
            label_y = start_y - i + 0.5  # Y-coordinate track alignment
            # Draw text
            ax.text(start_x - 0.2,
                    label_y,
                    variables[i],
                    ha='right',
                    va='center',
                    fontsize=12,
                    fontweight='bold')

            # Anchor mapping
            conn_x = start_x + i + 1  # X-coordinate
            conn_y = start_y - i + 0.5  # Y-coordinate
            connection_points.append((conn_x, conn_y))  # Append anchor tracking

        # Bottom X-axis labels
        for i in range(n):  # Iterate through columns
            label_x = start_x + i + 0.5  # Label X-coordinate allocation
            # Draw variable text markers
            ax.text(label_x,
                    start_y - (n - 1) - 0.5,
                    variables[i],
                    ha='center',
                    va='top',
                    fontsize=12,
                    fontweight='bold')
        # Draw Year stamp annotation
        ax.text(start_x,
                start_y - (n - 1) - 0.4,
                title_year,
                ha='right',
                va='center',
                fontsize=14,
                fontweight='bold')

    return connection_points

# =========================================================================================
# ===================================== 6. Main Plotting Function =====================================
# =========================================================================================
def create_complex_layout_plot(data, vars_list):
    fig, ax = plt.subplots(figsize=(20, 16))  # Create the canvas and coordinate axes
    ax.set_aspect('equal')  # Force equal aspect ratio to ensure cell blocks remain perfectly square
    n = len(vars_list)  # Number of variables
    gap_width = 2.0  # Set the middle gap width spacing

    start_x_2000 = -gap_width / 2 - n  # Starting X-coordinate for the top-left heatmap
    start_y_2000 = n + 2  # Starting Y-coordinate for the top-left heatmap

    start_x_2010 = -gap_width / 2 - n  # Starting X-coordinate for the bottom-left heatmap
    start_y_2010 = 0  # Starting Y-coordinate for the bottom-left heatmap

    start_x_2020 = gap_width / 2  # Starting X-coordinate for the top-right heatmap
    start_y_2020 = start_y_2000  # Starting Y-coordinate for the top-right heatmap

    # Plot the three triangular heatmaps
    pts_2000 = draw_triangle_heatmap(ax,
                                     data[2000]['corr'],
                                     data[2000]['p'],
                                     vars_list,
                                     start_x=start_x_2000,
                                     start_y=start_y_2000,
                                     type='top-left',
                                     title_year='2000')

    pts_2020 = draw_triangle_heatmap(ax,
                                     data[2020]['corr'],
                                     data[2020]['p'],
                                     vars_list,
                                     start_x=start_x_2020,
                                     start_y=start_y_2020,
                                     type='top-right',
                                     title_year='2020')

    pts_2010 = draw_triangle_heatmap(ax,
                                     data[2010]['corr'],
                                     data[2010]['p'],
                                     vars_list,
                                     start_x=start_x_2010,
                                     start_y=start_y_2010,
                                     type='bottom-left',
                                     title_year='2010')

    # Plot the central node
    center_y = ((start_y_2000 - n) + (start_y_2010 + 1)) / 2  # Y-coordinate for the central circle
    center_x = 0  # X-coordinate for the central circle

    c_face = selected_colors['center_circle_face']  # Fill color for the central circle
    c_text = selected_colors['center_text']  # Text color for the central circle

    # Draw the central large circle target
    ax.scatter(center_x,
               center_y,
               s=6000,
               marker='o',
               facecolor=c_face,
               edgecolor='none',
               zorder=100)

    # Plot the center node labels
    # Plot the first line of text inside the circle
    ax.text(center_x,
            center_y + 0.25,
            "SHDI",
            ha='center',
            va='bottom',
            fontsize=15,
            fontweight='bold',
            color=c_text,
            zorder=101)
    # Plot the second line of text inside the circle
    ax.text(center_x,
            center_y,
            "Pastoral area",
            ha='center',
            va='top',
            fontsize=13,
            fontweight='bold',
            color=c_text,
            zorder=101)

    # Internal network connection plotting function
    def draw_connections(points, year_data):
        for i, pt in enumerate(points):  # Iterate through each anchor point
            r_val = year_data['r'][i]  # Get the target correlation value corresponding to this variable
            style = get_line_style(r_val)  # Get the line style configuration based on correlation strength

            con = patches.ConnectionPatch(
                xyA=pt,
                xyB=(center_x, center_y),
                coordsA="data",
                coordsB="data",
                axesA=ax,
                axesB=ax,  # Configure coordinate systems
                color=style['color'],
                linewidth=style['linewidth'],
                linestyle=style['linestyle'],
                alpha=style.get('alpha', 1.0),
                zorder=1
            )
            ax.add_patch(con)  # Add the connection line to the layout
            # Draw a small dot marker at the anchor point location as a decorative flourish
            ax.scatter(pt[0], pt[1], s=40, facecolor='white', edgecolor='gray', zorder=20)

    draw_connections(pts_2000, data[2000])  # Draw network edges for 2000
    draw_connections(pts_2020, data[2020])  # Draw network edges for 2020
    draw_connections(pts_2010, data[2010])  # Draw network edges for 2010

    ax.set_xlim(start_x_2000 - 3, start_x_2020 + n + 3)  # X-axis display limits
    ax.set_ylim(start_y_2010 - n - 2, start_y_2000 + 3)  # Y-axis display limits
    ax.axis('off')  # Hide borders, frames, and ticks

    # Colorbar configuration
    cbar_ax = fig.add_axes([0.7, 0.25, 0.013, 0.25])  # Add a new axes container into the figure layout for the colorbar
    # Create the colorbar object
    cb = colorbar.ColorbarBase(cbar_ax,
                               cmap=current_cmap,
                               norm=norm,
                               orientation='vertical')
    cb.set_label("Pearson's r", size=18)  # Set colorbar title label
    cb.set_ticks([-1, -0.5, 0, 0.5, 1])  # Colorbar tick positions
    cb.outline.set_visible(False)  # Remove colorbar outer border frame
    cb.ax.tick_params(size=0, labelsize=18)  # Configure tick parameters (hide marks, set label size)

    # Legend configuration
    c_pos = selected_colors['heatmap_positive']  # Positive correlation color
    c_neg = selected_colors['heatmap_negative']  # Negative correlation color

    # Define legend element list, separated into two correlation groups
    legend_elements = [
        # Positive correlation group
        Line2D([0], [0], color=c_pos, lw=1.0, linestyle='--', alpha=0.5, label='Positive < 0.10'),
        Line2D([0], [0], color=c_pos, lw=1.5, linestyle='-', alpha=0.65, label='Positive 0.10 - 0.25'),
        Line2D([0], [0], color=c_pos, lw=3.0, linestyle='-', alpha=0.8, label='Positive 0.25 - 0.50'),
        Line2D([0], [0], color=c_pos, lw=5.0, linestyle='-', alpha=1.0, label='Positive > 0.50'),
        # Negative correlation group
        Line2D([0], [0], color=c_neg, lw=1.0, linestyle='--', alpha=0.5, label='Negative > -0.10'),
        Line2D([0], [0], color=c_neg, lw=1.5, linestyle='-', alpha=0.65, label='Negative -0.10 to -0.25'),
        Line2D([0], [0], color=c_neg, lw=3.0, linestyle='-', alpha=0.8, label='Negative -0.25 to -0.50'),
        Line2D([0], [0], color=c_neg, lw=5.0, linestyle='-', alpha=1.0, label='Negative < -0.50')
    ]

    # Plot figure legend
    ax.legend(handles=legend_elements,
              loc='lower right',
              bbox_to_anchor=(0.76, 0.2),
              title="Correlation Network (Lines)",
              frameon=False,
              fontsize=14,
              title_fontsize=16,
              ncol=1)

    # Figure panel sub-label annotation
    ax.text(start_x_2020 + n - 4, start_y_2010 - n + 1, "(a)", fontsize=24, fontweight='bold')

    # Save outputs
    plt.savefig(fr'D:\folder\combined_heatmap{COLOR_CHOICE}.png', dpi=300, bbox_inches='tight')
    plt.savefig(fr'D:\folder\combined_heatmap{COLOR_CHOICE}.pdf', bbox_inches='tight')

# =========================================================================================
# ====================================== 7. Main Program =======================================
# =========================================================================================

if __name__ == "__main__":
    vars_list = ['CS', 'FP', 'HQ', 'SR', 'WY']  # Features
    years = [2000, 2010, 2020]  # Sheets
    raw_data_file = r'D:\folder\raw_data.xlsx'  # Full path to the raw data file
    analysis_result_file = r'D:\folder\simulation_results.xlsx'  # Full path to the analysis results file

    # Call the data analysis function
    plot_data = analyze_raw_data(raw_data_file,
                                 analysis_result_file,
                                 years,
                                 vars_list)
    # Call the main plotting function
    create_complex_layout_plot(plot_data,
                               vars_list)

Thank you for reading.


메타데이터
post_id
1f49409464f0
slug
journal-figure-replication-plotting-a-composite-correlation-network-heatmap-with-python-1f49409464f0
url
https://medium.com/top-python-libraries/journal-figure-replication-plotting-a-composite-correlation-network-heatmap-with-python-1f49409464f0
canonical_url
https://medium.com/top-python-libraries/journal-figure-replication-plotting-a-composite-correlation-network-heatmap-with-python-1f49409464f0
author_url
https://medium.com/@benjamin_hui
status
ok
fetched_at
2026-06-14 16:15:44