当前位置:首页>python>期刊配图复现 | Python 绘制多面板分层热力图,完美展示混合模型性能增量

期刊配图复现 | Python 绘制多面板分层热力图,完美展示混合模型性能增量

  • 2026-10-21 13:28:19
期刊配图复现 | Python 绘制多面板分层热力图,完美展示混合模型性能增量

01

前言

你有没有遇到过这种情况:手里捏着好几个模型,跑出了几十个指标,Excel 表格拉得老长,结果导师看了一眼说:“这数据太多了,我看不过来,画张图吧。”

于是你打开 Python,画了 10 张柱状图,贴到论文里,审稿人又说:“版面太浪费,信息密度低。”

这时候,你需要的就是一张“高维数据浓缩矩阵”。

今天我们要复刻的这张图,来自 Water Resources Management (2024) 的一篇关于水文混合模型的论文 。作者想展示混合模型(Hybrid Models)相对于传统物理模型(HBV)和纯深度学习模型(LSTM)的性能提升 。

这张图的高明之处在于:它在一个版面内,优雅地装下了 4 个站点 × 2 种基准 × 3 个模型 × 5 个指标 = 120 个数据点,而且通过颜色深浅,让你一眼就能看出“谁在吊打谁”。

今天,我们就用 Python,把这张图从结构到逻辑完整复现,并把每一步“为什么这么做”讲得明明白白。拒绝“只给代码不讲理”,我们要的是能够迁移的绘图逻辑。

02

原图解析

在写代码之前,我们需要像外科医生一样,先对这张图进行“解剖”。很多同学画图画不像,不是因为代码写不对,而是因为根本没看懂原图的骨架。

  • 🦴 骨架 (Structure):

    这就不是一个简单的 plt.subplots(4, 2)。你仔细看,每一行的右侧都有一个独立的色条(Colorbar)。这意味着每一行其实是一个“左图 + 右图 + 色条”的组合。如果我们强行用标准网格去套,色条就会把子图挤扁。

    解决思路:我们需要 GridSpec,手动划分出 [1, 1, 0.05] 这样的宽度比例,给色条预留“VIP 专座”。

  • 🍰 核心技巧 (The Trick):

    注意看热力图的格子,它们之间有白色的间隙。这叫 "Gridline Separation"。别小看这根白线,没有它,相邻的色块会糊成一团;有了它,矩阵的颗粒度瞬间就出来了。

  • 🎨 配色 (Palette):

    原图使用了“蓝-白-黄绿”的配色。这里有一个极其关键的隐形逻辑:零点必须是白色的。

    因为这是“增量图”,正值(蓝色)代表性能提升,负值(黄绿)代表性能下降。如果 0 值偏到了蓝色或者绿色区域,整张图的科学含义就全错了。这在绘图中叫 Diverging Norm(发散归一化)。

论文原图

3.2 读懂神图 (Scientific Decoding)

别光顾着画,这图到底说了个啥?如果你看不懂图里的科学逻辑,画出来也是没有灵魂的躯壳。

结合论文 ,这张图展示了三个混合模型(HLUDC, HLBDC, HLBDCT)在四个站点((a) WS 到 (d) BB)的表现。

  • 横轴:NSE, KGE, r, alpha, beta 是五个水文评估指标 。

  • 读图逻辑:颜色越蓝,说明相对于基准模型(HBV 或 LSTM)的提升越大。

  • 关键发现:请看每一组小热力图的最后一行(HLBDCT),几乎全是深蓝色 。这意味着引入“时变参数(Time-varying parameters)”的 HLBDCT 模型,在几乎所有站点和指标上都稳稳地胜过了其他模型。

  • 异常点:在 (b) XHB 站点,右上角出现了大片的浅色甚至黄色区域,数值甚至是负的 。这是因为该区域数据稀缺,导致模型有点“水土不服”,但这恰恰展示了图表的诚实性——好的可视化不仅展示优点,也要暴露问题。

03

代码复刻

Step 1 全局配置

不要在每个 ax.plot 里重复写 linewidth=2。利用 rc_params 全局控制,既优雅又方便后期统一修改期刊要求的字号。这里我们特别配置了 mathtext,确保公式里的希腊字母 $\alpha$ 和 $\beta$ 使用 Times New Roman 字体,而不是默认的丑丑的数学字体。

import matplotlib.pyplot as pltimport matplotlib.font_manager as fmimport warnings# 全局关闭非关键警告,保证绘图输出整洁,看着清爽warnings.filterwarnings('ignore')# --- 顶刊风格内核锁定 (Journal Aesthetics) ---# 字体设置:优先使用 Times New Roman,这是 SCI 的标配plt.rcParams['font.family'] = ['Times New Roman', 'Arial', 'SimHei']plt.rcParams['mathtext.fontset'] = 'stix'       # 公式字体使用 STIX (类似 LaTeX)# 基础字号与线条定义:线条要粗,字要大,这就是“高级感”的来源plt.rcParams['font.size'] = 16                  # 默认字号plt.rcParams['axes.linewidth'] = 1.5            # 坐标轴线宽 (Bold)plt.rcParams['lines.linewidth'] = 2.0           # 数据线宽plt.rcParams['xtick.direction'] = 'in'          # 刻度朝内,更紧凑plt.rcParams['ytick.direction'] = 'in'plt.rcParams['savefig.bbox'] = 'tight'          # 自动切除白边plt.rcParams['savefig.dpi'] = 600               # 印刷级分辨率

Step 2 画布构建

这是本图最难的地方。如果你用 plt.subplots(4, 3),中间那列会变得巨宽,色条会变得巨粗。

我们要用 width_ratios 来控制:左图(1) : 右图(1) : 色条(0.05)。这就好比装修房子,给储物间(色条)预留的面积肯定不能和客厅(主图)一样大。

# 假设我们已经有了清洗好的数据结构 data_dict (按站点分层)# 定义 4 行,每行包含:左图、右图、色条位fig = plt.figure(figsize=(14, 10))# wspace=0.1 保证左右图紧凑,hspace=0.3 保证行间有空隙写标题gs = fig.add_gridspec(4, 3, width_ratios=[1, 1, 0.05],                       wspace=0.1, hspace=0.3)# 循环生成子图对象axes_matrix = []for i in range(4):    ax_left = fig.add_subplot(gs[i, 0])   # Compared to HBV    ax_right = fig.add_subplot(gs[i, 1])  # Compared to LSTM    ax_cbar = fig.add_subplot(gs[i, 2])   # Colorbar 专座    axes_matrix.append((ax_left, ax_right, ax_cbar))# 这样,我们就得到了一个布局完美的“骨架”,等待填充数据。

tep 3 归一化

这是热力图的灵魂。

如果你的数据范围是 [-5, 20],直接画出来,白色(0值)会偏向色条的左边。这会导致读者的直觉偏差。

我们需要手动计算 limit = max(|min|, |max|),然后强制设定 vmin=-limit, vmax=limit。这样,0 值永远被锁定在色条的正中央(白色)。

# 模拟一个站点的计算逻辑station_vals = [-5.2, 10.5, 3.1, -15.8, 0.2] # 假设这是该行的所有数据max_abs = max([abs(x) for x in station_vals])limit = np.ceil(max_abs)  # 向上取整,留点余地# 关键:对称归一化norm = mcolors.Normalize(vmin=-limit, vmax=limit)# 使用 Seaborn 绘图时传入这个 norm# sns.heatmap(..., vmin=-limit, vmax=limit, cmap="RdBu_r")

Step 4 数据映射

在绘制热力图时,除了颜色,我们还需要把具体的数值写在格子里。Seaborn 的 annot=True 很方便,但为了达到顶刊效果,我们需要根据背景颜色的深浅,自动反转文字颜色(深底白字,浅底黑字)。虽然 Seaborn 会自动处理,但在极值情况下手动微调更稳。

此外,注意 linewidths=1.5 的设置,这就是我们在解构环节提到的“切割线”。

# 核心绘图函数片段def plot_heatmap_panel(ax, data, vmin, vmax, cmap):    sns.heatmap(data, ax=ax,                 cmap=cmap,                 vmin=vmin, vmax=vmax,                annot=True,          # 显示数值                fmt=".3f",           # 保留3位小数                cbar=False,          # 先关掉自带色条,我们要用独立的                linewidths=1.5,      # 关键:格子间的白线                linecolor='white',   # 线的颜色                annot_kws={"size": 10, "weight": "bold", "family": "Times New Roman"})    # 移除多余的轴标签,保持清爽    ax.set_ylabel("")    ax.set_xlabel("")    # 增加黑色边框 (Spine) 增强对比度    for spine in ax.spines.values():        spine.set_visible(True)        spine.set_linewidth(1.0)

Step 5  细节修饰

最后一步,是给图表“化妆”。

  1. 斜体标签:左侧的模型名 HLUDC 是斜体。

  2. 希腊字母:X 轴的 alpha 和 beta 要转义为 LaTeX 格式。

  3. 独立色条连接:把刚才预留的 ax_cbar 用上。

# 处理 X 轴的特殊字符labels_raw = ['NSE', 'KGE', 'r', 'alpha', 'beta']labels_formatted = [    r'$\alpha$' if 'alpha' in t else (r'$\beta$' if 'beta' in t else t)     for t in labels_raw]ax.set_xticklabels(labels_formatted, fontweight='bold', fontstyle='italic')# 手动挂载色条# sm 是 ScalarMappable,存储了颜色映射关系sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)sm.set_array([]) cbar = fig.colorbar(sm, cax=ax_cbar, orientation='vertical')# 根据数值大小动态调整色条刻度显示(整数或小数)if limit > 10:    cbar.ax.yaxis.set_major_formatter(plt.FormatStrFormatter('%.0f'))else:    cbar.ax.yaxis.set_major_formatter(plt.FormatStrFormatter('%.1f'))

让我们看看最终的复刻效果与原图的对比(左:复刻图;右:原图 )。

  • 还原度:我们不仅还原了 4x2 的布局,连每个色块的数值精度都完全一致。

  • 改进点:原图在负值区域使用了一种青绿色,而我们使用了区分度更高的黄绿色。在视觉心理学上,这种对比色能更强烈地警示“性能下降”。

  • 技术闭环:通过 GridSpec 解决了布局难题,通过 Symmetric Norm 解决了科学逻辑表达,通过 rcParams 解决了字体审美。

复刻图

04

多配色参考

多配色参考

05

代码获取

👇 关注公众号【嗡嗡的Python日常】

🚫 关于源码: 本文核心代码为原创定制,暂不免费公开。

✅ 如果你需要:

  • 购买本项目完整源码 + 数据

  • 定制类似的科研绘图

  • 咨询代码运行报错问题

请直接添加号主微信沟通(有偿分享☕️): Wjtaiztt0406

微信号丨Wjtaiztt0406

最新文章

随机文章