科研绘图新技能:如何用Python绘制精美山脊图!

学术   2024-06-18 09:51   日本  

山脊图(Ridge Plot)是一种用于可视化数据分布的图表类型,它结合了直方图和密度图的特点,能够清晰地展示单个变量或多个变量之间的分布情况,并且可以显示不同类别或组之间的比较。

山脊图通常用于探索数据集中的多个变量之间的关系,或者用于比较不同组别之间的分布差异。它们特别适用于数据集中存在多个分布并且想要直观比较这些分布的情况。

在山脊图中,每个变量或组别的分布被绘制为一条“山脊”,因此得名。这些“山脊”是通过将直方图或密度图沿着纵轴堆叠而成的,每个“山脊”代表数据的一个子集或分组。

▌山脊图的绘制

    1. 多组数据密度图

在上一篇推文中我们介绍了密度图的绘制,考虑到山脊图与密度图的相似性,在绘制山脊图之前,首先向大家介绍同一坐标系中绘制多组数据密度图的方法。假设我们有一个包含多个蛋白质表达水平数据的Excel文件 protein_data.xlsx,其中每一列代表一个独立的数据集。我们希望通过多组数据密度图来展示这些数据的分布情况。

为了使图表美观,我们使用 Seaborn 库来设置图形风格,并且使用 Seaborn 的 kdeplot 函数在同一坐标系中绘制多组数据密度图,如图 1 所示。

图 1. 多组数据密度图绘制实例


图 1 的绘图代码如下所示:


import os  
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd

# 读取Excel文件中的数据
df = pd.read_excel('protein_data.xlsx', sheet_name='Sheet1')

# 设置图形风格和背景色
sns.set(style="white")

# 创建多组数据密度图
sns.kdeplot(data=df, fill=True, alpha=0.8, palette="viridis", linewidth=0)

# 添加标签
plt.xlabel("Expression Level")

# 调整图像布局,确保所有元素居中
plt.tight_layout()

# 保存图像
output_path = '图1 多组数据密度图绘制实例.png'
os.makedirs(os.path.dirname(output_path), exist_ok=True)
plt.savefig(output_path, dpi=600)
plt.show()

2. 一般山脊图

多组数据密度图虽然可以直观地比较不同数据列的分布,但当数据列较多时,图表可能会显得杂乱。这时,我们可以考虑使用山脊图,它通过将每个数据列的密度图沿垂直方向偏移,形成层叠效果,从而更加清晰地展示每个数据列的分布情况。

图 2 展示了使用 Seaborn 绘制山脊图的基本样式,我们首先提供完整的绘图代码,然后再详细解释各行代码所代表的含义,方便大家理解和应用。

图 2. 山脊图绘制实例


图 2 的绘图代码如下所示:


import os  
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

# 设置主题风格和背景
sns.set_theme(style="white", rc={"axes.facecolor": (0, 0, 0, 0)})

# 读取Excel文件中的数据
df = pd.read_excel('protein_data.xlsx')

# 将数据转换为长格式,方便Seaborn处理
df_long = df.melt(var_name='Group', value_name='Expression Level')

# 初始化FacetGrid对象
pal = sns.cubehelix_palette(len(df.columns), rot=-.25, light=.7)
g = sns.FacetGrid(df_long, row='Group', hue='Group', aspect=15, height=.5, palette=pal)

# 绘制密度图
g.map(sns.kdeplot, 'Expression Level', bw_adjust=.5, clip_on=False, fill=True, alpha=1, linewidth=1.5)
g.map(sns.kdeplot, 'Expression Level', clip_on=False, color="w", lw=2, bw_adjust=.5)

# 添加基线
g.refline(y=0, linewidth=1, linestyle="-", color=None, clip_on=False)

# 定义并使用一个简单的函数在轴坐标中标注图形
def label(x, color, label):
ax = plt.gca()
ax.text(0, .2, label, fontweight="bold", color=color, ha="left", va="center", transform=ax.transAxes)

g.map(label, "Expression Level")

# 设置子图的重叠
g.figure.subplots_adjust(hspace=-.5)

# 移除不必要的轴细节
g.set_titles("")
g.set(yticks=[], ylabel="")
g.despine(bottom=True, left=True)

# 设置横坐标标题
g.set_axis_labels("Expression Level")

# 设置图像大小
g.fig.set_size_inches(4, 4) # 设置图像的宽度和高度

# 调整图像布局,确保所有元素居中
plt.tight_layout()

# 保存图像
output_path = '图2 山脊图绘制实例.png'
os.makedirs(os.path.dirname(output_path), exist_ok=True)
plt.savefig(output_path, dpi=600)

# 显示图形
plt.show()


下面对图 2 的代码进行解释,实际应用时可根据需求修改参数,绘制适合的图像。


# 设置主题风格和背景 
sns.set_theme(style="white", rc={"axes.facecolor": (0, 0, 0, 0)})


这里设置了Seaborn的主题风格为白色,同时设置了背景颜色为透明。


# 读取Excel文件中的数据
df = pd.read_excel('protein_data.xlsx')

# 将数据转换为长格式,方便Seaborn处理
df_long = df.melt(var_name='Group', value_name='Expression Level')


我们从Excel文件中读取数据,并将其转换为长格式(long format)。长格式的DataFrame有两列,一列为组别(Group),一列为表达水平(Expression Level),这样可以方便Seaborn进行处理和绘图。


# 初始化FacetGrid对象
pal = sns.cubehelix_palette(len(df.columns), rot=-.25, light=.7)
g = sns.FacetGrid(df_long, row='Group', hue='Group', aspect=15, height=.5, palette=pal)
  • sns.cubehelix_palette:生成一个颜色调色板,适合连续的数据。len(df.columns)确定了调色板中的颜色数量,rot参数控制调色板的旋转,light参数控制调色板的亮度。

  • sns.FacetGrid:创建一个Facets网格对象,用于将数据按行(row)或列(col)进行分割绘图。这里按组别(Group)分行,每一行一个子图。

    • row='Group':指定按哪一列来分行。

    • hue='Group':指定颜色的分类依据。

    • aspect=15:每个子图的宽高比。

    • height=.5:每个子图的高度(英寸)。

    • palette=pal:指定调色板。


# 绘制密度图
g.map(sns.kdeplot, 'Expression Level', bw_adjust=.5, clip_on=False, fill=True, alpha=1, linewidth=1.5)
g.map(sns.kdeplot, 'Expression Level', clip_on=False, color="w", lw=2, bw_adjust=.5)


g.map:在FacetGrid的每个子图上绘制指定的图形。

  • sns.kdeplot:绘制核密度估计(KDE)图。

  • Expression Level:要绘制的变量。

  • bw_adjust=.5:调整核密度估计的带宽,值越小,曲线越平滑。

  • clip_on=False:是否在图形区域内裁剪元素。

  • fill=True:填充曲线下面的区域。

  • alpha=1:透明度,1表示不透明。

  • linewidth=1.5:曲线的宽度。

  • color="w"lw=2:为第二次绘制的曲线设置白色和更宽的线条,用于增加曲线的对比度。


# 添加基线
g.refline(y=0, linewidth=1, linestyle="-", color=None, clip_on=False)


g.refline:在每个子图上添加参考线。

  • y=0:在y=0的位置添加基线。

  • linewidth=1:线条宽度。

  • linestyle="-":线条样式,实线。

  • color=None:使用默认颜色。

  • clip_on=False:是否在图形区域内裁剪元素。


# 定义并使用一个简单的函数在轴坐标中标注图形
def label(x, color, label):
ax = plt.gca()
ax.text(0, .2, label, fontweight="bold", color=color, ha="left", va="center", transform=ax.transAxes)

g.map(label, "Expression Level")


label函数:用于在每个子图上添加标签。

  • ax.text:在指定位置添加文本。

    • 0, .2:文本的x和y位置,轴坐标(0到1)。

    • label:要显示的文本。

    • fontweight="bold":文本字体加粗。

    • color=color:文本颜色。

    • ha="left":水平对齐方式,左对齐。

    • va="center":垂直对齐方式,居中。

    • transform=ax.transAxes:使用轴坐标。


# 设置子图的重叠
g.figure.subplots_adjust(hspace=-.5)


g.figure.subplots_adjust:调整子图之间的空间。

  • hspace=-.5:控制行之间的间距,负值表示子图会重叠。


# 移除不必要的轴细节
g.set_titles("")
g.set(yticks=[], ylabel="")
g.despine(bottom=True, left=True)
  • g.set_titles(""):移除子图标题。

  • g.set(yticks=[], ylabel=""):移除y轴刻度和标签。

  • g.despine(bottom=True, left=True):移除底部和左侧的脊线(坐标轴)。


其余的代码应该比较熟悉了,这里就不再赘述。以上代码最终生成一个包含多个子图的山脊图,每个子图对应一个组别的表达水平分布,并进行了美观的布局和标注。

3. 带有均值参考线和点的山脊图

为了更好的展示数据分布情况,下面我们在山脊图上添加每组数据的均值点和全局均值参考线,以方便比较不同组之间的中心位置和每个组的均值相对于整体均值的位置。如图 3 所示。

图 3. 添加平均参考线和点的山脊图绘制实例


图 3 的核心代码如下所示,我们使用 axvlin() 和 scatter() 添加了全局均值参考线和每个组的均值点。


# 绘制密度图  
g.map(sns.kdeplot, 'Expression Level', bw_adjust=.5, clip_on=False, fill=True, alpha=1, linewidth=1.5)
g.map(sns.kdeplot, 'Expression Level', clip_on=False, color="w", lw=2, bw_adjust=.5)

# 计算全局均值
global_mean = df_long['Expression Level'].mean()

# 添加每组的均值参考点和全局均值参考线
def add_means(data, **kwargs):
ax = plt.gca()
group_means = data.groupby('Group')['Expression Level'].mean()

for group, mean in group_means.items():
ax.scatter([mean], [0.01], color='black', s=10) # 每组的均值点
ax.axvline(global_mean, color='#525252', linestyle='--') # 全局均值参考线

g.map_dataframe(add_means)

# 添加基线
g.refline(y=0, linewidth=1, linestyle="-", color=None, clip_on=False)

4. 带有分位数的山脊图

除了均值参考线和点之外,还可以添加分位数标记,以便了解数据在不同区间的分布情况,包括数据的集中程度、离散程度以及是否存在异常值。通过观察分位数,我们可以更好地理解数据的整体形状和特征。如图 4 所示。

图 4. 添加分位数山脊图绘制实例


图 4 的核心代码如下所示,quantiles = np.percentile(subset['Expression Level'], [2.5, 10, 25, 75, 90, 97.5]): 这一行代码计算每个组的表达水平的分位数。在这个例子中,我们计算了2.5th、10th、25th、75th、90th和97.5th分位数。然后通过 ax.fill_betweenx() 函数,为每个组的数据添加了分位数填充。


# 定义易区分的填充颜色  
fill_colors = ['#96e6a1', '#a5f6a1', '#d4fc79', '#a5f6a1', '#96e6a1']

# 绘制密度图
g.map(sns.kdeplot, 'Expression Level', bw_adjust=.5, clip_on=False, fill=True, alpha=1, linewidth=1.5)
g.map(sns.kdeplot, 'Expression Level', clip_on=False, color="w", lw=2, bw_adjust=.5)

# 计算全局均值
global_mean = df_long['Expression Level'].mean()

# 添加每组的均值点、全局均值参考线和分位数填充
def add_means_and_quantiles(data, **kwargs):
ax = plt.gca()
group_means = data.groupby('Group')['Expression Level'].mean()

for group in data['Group'].unique():
subset = data[data['Group'] == group]
mean = group_means[group]
quantiles = np.percentile(subset['Expression Level'], [2.5, 10, 25, 75, 90, 97.5])

# 填充分位数间的区域
for j in range(len(quantiles) - 1):
ax.fill_betweenx(
[0, 0.01], # y 轴的范围
quantiles[j], # 下分位数
quantiles[j + 1], # 上分位数
color=fill_colors[j],
alpha=0.9
)

# 每组的均值点
ax.scatter([mean], [0.005], color='black', s=8)

# 全局均值参考线
ax.axvline(global_mean, color='#525252', linestyle='--')

g.map_dataframe(add_means_and_quantiles)

# 添加基线
g.refline(y=0, linewidth=1, linestyle="-", color=None, clip_on=False)

如果您喜欢我们的文章,欢迎关注

更多科研干货点击👇,输入关键词搜索🔍

叮当学术
📚零零碎碎的科研学习记录~🔬科研本沉闷,但跑起来有风。
 最新文章