当前位置:首页>python>一天一个科研小技巧——Python复刻顶刊云雨图与分位数连接对齐图

一天一个科研小技巧——Python复刻顶刊云雨图与分位数连接对齐图

  • 2026-09-09 18:06:34
一天一个科研小技巧——Python复刻顶刊云雨图与分位数连接对齐图

是不是总羡慕 CNS 顶刊里那些既展示数据全貌、又透着高级感的统计图?📊✨

明明是同样的数据,别人画出来是“艺术品”🎨,我们画出来却是平平无奇的柱状图。

别急,今天就用两段 Python 代码,带你复刻顶刊里出镜率超高的

云雨图(Raincloud Plot)⛅ 和分位数连接对齐图🔗,让你的下一篇文章配图瞬间提升格调!📈


☁️🌧️ 云雨图:三合一信息黄金组合

云雨图其实是一个“组合拳”💪——它把散点 + 箱线图 + 半边小提琴密度图放在一起。

  • 散点:展示原始数据分布,拒绝被均值掩盖的离群值
  • 箱线图:一眼看清四分位数、中位数和异常值
  • 半边小提琴:平滑展示数据密度,哪里数据多一目了然

顶刊论文常用它来同时对比多个组别,例如不同处理下的土壤有机碳(SOC)🌱 或总氮(TN)含量。下面我们就用 Python 的matplotlib把它画出来。👀数据运用随机数替代:

🐍 核心代码思路

import numpy as npimport matplotlib.pyplot as pltfrom scipy.stats import gaussian_kdenp.random.seed(42)n_samples = 30groups = ['Conven_Amb', 'Conven_Warm', 'Conserv_Amb', 'Conserv_Warm']data_f = {    'Conven_Amb': np.random.normal(8.8, 0.4, n_samples),    'Conven_Warm': np.random.normal(8.9, 0.35, n_samples),    'Conserv_Amb': np.random.normal(12.8, 0.5, n_samples),    'Conserv_Warm': np.random.normal(13.4, 0.6, n_samples)}data_g = {    'Conven_Amb': np.random.normal(0.9, 0.1, n_samples),    'Conven_Warm': np.random.normal(1.0, 0.1, n_samples),    'Conserv_Amb': np.random.normal(1.48, 0.08, n_samples),    'Conserv_Warm': np.random.normal(1.55, 0.12, n_samples)}colors = ['#2B3A8F', '#C82538', '#1A8787', '#F2A93B']def draw_raincloud(ax, data_dict, ylabel, ylims, sig_pairs):    for i, grp in enumerate(groups):        y = data_dict[grp]        x_base = i        # 抖动散点        x_jitter = x_base - 0.2 + np.random.uniform(-0.05, 0.05, len(y))        ax.scatter(x_jitter, y, color=colors[i], alpha=0.4, s=40, zorder=1)        # 箱线图        box = ax.boxplot(y, positions=[x_base - 0.05], widths=0.18,                         patch_artist=True, showfliers=False, zorder=2)        plt.setp(box['boxes'], facecolor='none', edgecolor=colors[i], linewidth=2)        plt.setp(box['whiskers'], color=colors[i], linewidth=2)        plt.setp(box['caps'], color=colors[i], linewidth=2)        plt.setp(box['medians'], color=colors[i], linewidth=2.5)        # 半边小提琴 (KDE)        kde = gaussian_kde(y)        y_grid = np.linspace(y.min() - 0.2, y.max() + 0.2, 100)        kde_val = kde(y_grid)        kde_val = (kde_val / kde_val.max()) * 0.18        ax.plot(x_base + 0.1 + kde_val, y_grid, color=colors[i], linewidth=2, zorder=3)        ax.fill_betweenx(y_grid, x_base + 0.1, x_base + 0.1 + kde_val,                         color=colors[i], alpha=0.05)    ax.set_xticks(range(len(groups)))    ax.set_xticklabels(groups, rotation=20, ha='right', fontsize=11)    ax.set_ylabel(ylabel, fontsize=12)    ax.set_ylim(ylims)    for p1, p2, h, line_h in sig_pairs:        ax.plot([p1 - 0.05, p2 + 0.1], [h, h], color='black', linewidth=1.2)        ax.text((p1 + p2) / 2, line_h, '***', ha='center', va='bottom',                color='black', fontsize=14, weight='bold')fig1, (ax_f, ax_g) = plt.subplots(1, 2, figsize=(10, 4.5))draw_raincloud(ax_f, data_f, "SOC (g C kg$^{-1}$)", (7.5, 15.5), [(2, 3, 14.8, 14.5)])ax_f.text(-0.15, 1.02, '(a)', transform=ax_f.transAxes, fontsize=14, weight='bold')draw_raincloud(ax_g, data_g, "TN (g N kg$^{-1}$)", (0.5, 1.9), [(0, 1, 1.4, 1.37), (2, 3, 1.75, 1.72)])ax_g.text(-0.15, 1.02, '(b)', transform=ax_g.transAxes, fontsize=14, weight='bold')plt.tight_layout()plt.show()
只需调节x_base的偏移量,就能将三个元素无缝拼接在一起。🧩 再添上显著星号(***)⭐ 和个性配色 🎨,你的第一幅顶刊风格云雨图就诞生啦!🎉

🔗 分位数连接对齐图:比较两个分布的神器

有时候我们不仅要对比均值,还要看整个分布形状的差异——比如肿瘤微环境中 🧬,不同分期下免疫细胞间的“距离”分布是否右偏、是否集中。

这时候,镜像密度曲线 + 分位数连线的图形就派上用场了!

这种图的上半部分是组1的核密度曲线,下半部分是组2的核密度曲线(翻转)🔄,中间用连线把相同分位数的点连起来。如果两条线对应的分位数点很靠近,连线就比较短,说明分布相似;偏离大的分位数点则能一眼看出差异位置。👀数据运用随机数替代:

🐍 核心代码思路

import numpy as npimport matplotlib.pyplot as pltimport seaborn as snsfrom scipy.stats import gaussian_kdefrom matplotlib.patches import Patchfrom matplotlib.lines import Line2Dnp.random.seed(42)n_dist_samples = 200dist_blue = np.random.chisquare(df=4, size=n_dist_samples) * 80 + 100dist_pink = np.random.chisquare(df=5, size=n_dist_samples) * 95 + 150def draw_quantile_alignment(ax, d1, d2, label_top, label_bottom, legend_title):    kde1 = gaussian_kde(d1)    kde2 = gaussian_kde(d2)    x_grid = np.linspace(0, 1700, 500)    # 上半部分密度 (蓝)    ax.fill_between(x_grid, 0.5, 0.5 + kde1(x_grid) * 120,                    color='#7A92C3', alpha=0.7, edgecolor='black', linewidth=0.5)    # 下半部分密度 (粉)    ax.fill_between(x_grid, -0.5, -0.5 - kde2(x_grid) * 120,                    color='#E2858A', alpha=0.7, edgecolor='black', linewidth=0.5)    # 基准线    ax.plot([0, 1100], [0.45, 0.45], color='black', linewidth=1)    ax.plot([0, 1100], [-0.45, -0.45], color='black', linewidth=1)    quantiles = np.linspace(0.1, 0.9, 9)    q1_vals = np.percentile(d1, quantiles * 100)    q2_vals = np.percentile(d2, quantiles * 100)    for i, (q1, q2) in enumerate(zip(q1_vals, q2_vals)):        is_ns = i < 3 and legend_title == "TNM"        line_color = '#D3D3D3' if is_ns else 'black'        marker_color = '#D3D3D3' if is_ns else 'black'        ax.plot([q1, q2], [0.45, -0.45], color=line_color, linewidth=1.2,                marker='o', markersize=5, markerfacecolor=marker_color,                markeredgecolor=marker_color)    ax.set_xlabel("Distance (pixels)", fontsize=11)    ax.set_ylabel("Group", fontsize=11)    ax.set_xlim(-50, 1700)    ax.set_ylim(-1.5, 1.5)    ax.set_yticks([])    ax.text(900, 0.1, "Kolmogorov-Smirnov\ntest: $P = 0$", fontsize=9, va='center')    ax.text(900, -0.2, "Wilcoxon signed-rank\ntest: $P = 1.41 \\times 10^{-16}$",            fontsize=9, va='center')    if legend_title == "TNM":        legend_elements = [            Line2D([0], [0], marker='o', color='none', markerfacecolor='#D3D3D3',                   markeredgecolor='#D3D3D3', label='NS'),            Line2D([0], [0], marker='o', color='none', markerfacecolor='black',                   markeredgecolor='black', label='$P < 0.05$'),            Patch(facecolor='#E2858A', edgecolor='black', alpha=0.7, label=label_bottom),            Patch(facecolor='#7A92C3', edgecolor='black', alpha=0.7, label=label_top)        ]    else:        legend_elements = [            Line2D([0], [0], marker='o', color='none', markerfacecolor='black',                   markeredgecolor='black', label='$P < 0.05$'),            Patch(facecolor='#E2858A', edgecolor='black', alpha=0.7, label=label_bottom),            Patch(facecolor='#7A92C3', edgecolor='black', alpha=0.7, label=label_top)        ]    ax.legend(handles=legend_elements, loc='center right', title=legend_title,              frameon=False, bbox_to_anchor=(1.5, 0.2), fontsize=10, title_fontsize=11)fig2, (ax_g2, ax_h) = plt.subplots(1, 2, figsize=(11, 3.5))draw_quantile_alignment(ax_g2, dist_blue, dist_pink, "Stages I & II", "Stages III & IV", "TNM")ax_g2.set_title("Distance between macrophage_\nVIM$^+$ cells and T$_{reg}$ cells", fontsize=11, pad=10)ax_g2.text(-0.08, 1.05, 'a', transform=ax_g2.transAxes, fontsize=14, weight='bold')draw_quantile_alignment(ax_h, dist_blue * 0.95, dist_pink * 1.05, "No", "Yes", "EHM")ax_h.set_title("Distance between macrophage_\nVIM$^+$ cells and T$_{reg}$ cells", fontsize=11, pad=10)ax_h.text(-0.08, 1.05, 'b', transform=ax_h.transAxes, fontsize=14, weight='bold')sns.despine(left=False, bottom=False)plt.tight_layout()plt.show()

🧵 绘图关键步骤

计算核密度📐:用gaussian_kde获得平滑密度。

绘制镜像密度🪞:上面用fill_between填充正值区域,下面用负值区域并翻转,形成对称效果。

连接分位点🔗:取 10%~90% 分位数,在两个密度曲线上定位并用直线连接,根据统计检验(如 KS 检验)用不同颜色标注是否显著 🔴⚫。

给一组对比加上“NS”(不显著) 灰色标记 ⚪,另一组显著标黑色 ⚫,再配上手工定制 Legend 🏷️,一张美观又信息密集的分布对比图即刻完成。

🤔 为何科研绘图要这么“卷”?

✅审稿人友好🤝:数据展示更透明,避免单纯柱状图掩盖分布差异。

✅一图多用💡:一张云雨图可同时替代散点图、箱线图和直方图,节约版面。

✅故事感强📖:分位数连接图能清晰叙述“在哪段分布上不同”,让统计差异变得可视化。

这两类图形尤其适合环境科学 🌍、生物医学 🧪、心理学 🧠 等领域中需要展示多组、非正态数据的场景。


📦 拿来即用的完整代码

觉得有用的话,点个「在看」👍 或转发给同门,一起卷起来~

我们下一个小技巧见 👋😊

最新文章

随机文章