Skip to content

matplotlib高级绘图:从基础到学术论文级 ​

5月24日2026年

先验直觉:iris_df = pd.DataFrame(iris.data, columns=iris.feature_names)

关键词:Python,matplotlib,pandas,PCA,评分卡,分类,时间序列


一、为什么需要高级绘图

python
# 数据准备:本文所有示例共用
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_iris, load_diabetes
import pandas as pd
import datetime
from matplotlib.gridspec import GridSpec
from matplotlib import rcParams

today = datetime.date.today().strftime('%Y-%m-%d')
np.random.seed(42)

# 基础函数数据
x = np.linspace(0, 10, 100)
y1 = np.sin(x) + np.random.normal(0, 0.1, 100)
y2 = np.cos(x) + np.random.normal(0, 0.1, 100)
y3 = np.sin(x) * np.cos(x)

# Iris数据集
iris = load_iris()
iris_df = pd.DataFrame(iris.data, columns=iris.feature_names)
iris_df['species'] = iris.target

默认的 plt.plot() 满足快速查看需求,但学术论文、公众号文章需要更高标准的可视化。一套好的配色加上合理排版,能让同样数据传递的信息量翻倍。学术审稿人往往从图表的第一印象判断研究的严谨程度——模糊的图片、刺眼的彩虹配色、重叠的文字,都会拉低整体评价。在实际工作中,同样的数据用默认设置画出来和专业级别画出来,给读者带来的感受天差地别。这不仅是审美的差异,更是数据传达效率的差异。

很多初学者在学完基础的折线图、柱状图、散点图之后,就以为自己"会画图了"。但真正进入科研论文写作或数据分析报告制作时,才发现自己掌握的远远不够:不知道如何在一张画布上排布多个子图、不知道如何选择适合数据的配色方案、不知道如何输出符合期刊要求的图片格式。这些看似琐碎的问题,恰恰是区分"能用"和"好用"的关键。

本文从 matplotlib 三层架构 出发,系统覆盖以下核心内容:子图排版技巧(包括 subplot_mosaic 和 GridSpec 两种方式)、颜色映射的选择原则(顺序型、发散型、定性型的应用场景)、Tufte 排版原则与 data-ink ratio 优化、高级图表类型(小提琴图、六边形分箱图、配对图)、主题自定义(rcParams 精细调参与 seaborn 风格切换)、输出优化(不同格式与 DPI 选择),以及中文用户最常见的踩坑指南。每一节均配有可直接运行的完整代码、预期输出说明,以及实际项目中常见的坑和解决方案。

二、matplotlib的三层架构

理解 matplotlib 的内部结构是进阶使用的前提。整个绘图系统分为三个层级:

层级名称作用
Figure(画布)顶层容器容纳所有元素,是整个图像的顶级对象
Axes(坐标系)中间层每个子图对应一个 Axes 对象,包含数据区域、坐标轴、刻度等
Artist(绘制元素)底层所有可见元素——线条、文本、图块、图例——都是 Artist 实例
python
# 三层架构演示:从画布到坐标系到绘图元素
fig = plt.figure(figsize=(10, 5))            # Figure层:创建画布
ax = fig.add_subplot(111)                     # Axes层:在画布上加坐标系
line = ax.plot(x, y1, color='#2980b9')[0]     # Artist层:绘制线条(返回Line2D对象)
ax.set_title('QIAN DATA: 三层架构示意图', fontsize=13)
ax.set_xlabel('x'); ax.set_ylabel('y')
plt.tight_layout()
plt.savefig('three_layer.png', dpi=150)
plt.show()

three_layer.png

预期输出: 一张带有正弦曲线的折线图,标题标注"QIAN DATA: 三层架构示意图"。

实用要点:

  • 操作 Artist 对象可以精细控制每个元素,例如 line.set_linewidth(3) 加粗线条,line.set_color('red') 改变颜色
  • 通过 ax.get_children() 可查看当前 Axes 上的所有 Artist 列表,用于调试和探索
  • 删除某个元素:调用 line.remove() 将其从 Axes 中移除
  • 理解三层架构后,复杂布局(如多个 Axes 共享一个 Figure)就不再困难——你可以在同一个 Figure 上创建任意数量的 Axes,每个 Axes 独立绘图,通过 Figure 统一控制输出大小和样式
  • 进阶技巧: 在同一个 Axes 上添加第二个 Y 轴(twinx())实际上是在同一个 Figure 上创建了第二个 Axes,二者共享 X 轴但 Y 轴独立,这正是双Y轴图的底层原理

三、颜色映射(Colormap)的选择原则

颜色映射是可视化中容易被忽视但极其重要的环节。选错配色不仅影响美观,更可能误导读者。matplotlib 提供三大类 colormap:

3.1 顺序型(Sequential) ​

适用于取值从低到高连续变化的数据(如温度、海拔、概率密度)。

python
# 顺序型 colormap 演示
data = np.random.rand(10, 10)
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
cmaps_seq = ['Blues', 'viridis', 'plasma']
for ax, cmap in zip(axes, cmaps_seq):
    im = ax.imshow(data, cmap=cmap, aspect='auto')
    ax.set_title(f'QIAN DATA: {cmap}(顺序型)')
    plt.colorbar(im, ax=ax, shrink=0.8)
plt.tight_layout()
plt.savefig('sequential_cmap.png', dpi=150)
plt.show()

sequential_cmap.png

预期输出: 三张热力图并排,分别使用 Blues、viridis、plasma 三种顺序型配色,颜色从浅到深递进表示数值从小到大。

推荐顺序型配色: viridis(色盲友好,科学计算标准,从深紫渐变到明黄)、Blues(清爽蓝色系,适合公众号文章和经济类图表)、plasma(暖色系,高对比度,适合强调极值区域的场景)。此外还有 magma(暗色背景下的好选择)、cividis(另一种色盲友好方案)。

特别提醒——哪些配色应该永久避开: jet(彩虹色)虽然看起来"鲜艳",但存在三个问题:一是色盲人群无法区分红绿色区域,二是印刷后中间段变暗导致细节丢失,三是视觉上创造了不存在的边界。同样不建议使用的还有 rainbow、gist_ncar、hsv 等高饱和度的彩虹色调色板。如果你的论文中还出现 jet 配色,审稿人大概率会建议你换掉。

3.2 发散型(Diverging) ​

适用于数据围绕某个中点(如零值、均值)向两侧偏离的场景。中点用浅色,两端用对比色。

python
# 发散型 colormap 演示
data_div = np.random.randn(10, 10)
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
cmaps_div = ['RdBu', 'coolwarm', 'PiYG']
for ax, cmap in zip(axes, cmaps_div):
    im = ax.imshow(data_div, cmap=cmap, center=0, aspect='auto')
    ax.set_title(f'QIAN DATA: {cmap}(发散型)')
    plt.colorbar(im, ax=ax, shrink=0.8)
plt.tight_layout()
plt.savefig('diverging_cmap.png', dpi=150)
plt.show()

diverging_cmap.png

预期输出: 三张热力图,颜色从两端深色向中间浅色过渡,适合表达正负差异或偏离程度。

推荐发散型配色: RdBu(红蓝对比最经典)、coolwarm(冷暖色调,视觉舒适)、PiYG(紫-绿,适合生物/医学)。

3.3 定性型(Qualitative) ​

适用于离散类别数据,各类别之间没有大小关系,颜色只需在视觉上互相区分即可。

python
# 定性型 colormap 演示
categories = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
values = np.random.randint(5, 20, 8)
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
cmaps_qual = ['Set2', 'tab10', 'Pastel1']
for ax, cmap in zip(axes, cmaps_qual):
    colors = plt.cm.get_cmap(cmap)(np.linspace(0, 1, len(categories)))
    ax.bar(categories, values, color=colors, edgecolor='white')
    ax.set_title(f'QIAN DATA: {cmap}(定性型)')
plt.tight_layout()
plt.savefig('qualitative_cmap.png', dpi=150)
plt.show()

qualitative_cmap.png

预期输出: 三组柱状图,每组使用不同的定性配色方案,各类别颜色不重叠且视觉区分度高。

推荐定性型配色: Set2(莫兰迪色系,柔和)、tab10(对比强烈,适合分类数少的情形)、Pastel1(粉彩色系,适合背景元素)。

三类 colormap 选择总结:

数据类型推荐 colormap 类型示例
连续数值(0→100)顺序型viridis, Blues, plasma
有中点的数值(-1→0→1)发散型RdBu, coolwarm, PiYG
离散类别(A/B/C/D)定性型Set2, tab10, Pastel1
避免使用—jet, rainbow, gist_ncar

四、排版原则:Tufte原则与data-ink ratio

Edward Tufte 在《The Visual Display of Quantitative Information》中提出了影响深远的数据可视化原则,至今仍是学术图表制作的黄金标准。

核心思想:

原则解释
Data-ink ratio数据"墨水"占总墨水比例最大化,非数据元素最小化
Avoid chartjunk去除没有数据意义的装饰——3D伪影、过度网格、无意义渐变、冗余图片
Lie factor图形尺寸变化应与数据变化成比例,不能人为夸大视觉效果
Small multiples多变量对比时用小倍数图阵列,而非将所有信息堆叠在同一张图中
python
# data-ink ratio 对比:默认 vs Tufte优化
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

# 左图:默认风格(low data-ink ratio)
ax1.plot(x, y1, color='b', linewidth=1)
ax1.set_title('默认风格(低 data-ink ratio)', fontsize=11)
ax1.grid(True)
ax1.set_facecolor('#f5f5f5')

# 右图:Tufte风格(high data-ink ratio)
ax2.plot(x, y1, color='#2c3e50', linewidth=1.5)
ax2.set_title('QIAN DATA: Tufte优化风格(高 data-ink ratio)', fontsize=11)
ax2.spines['top'].set_visible(False)
ax2.spines['right'].set_visible(False)
ax2.spines['left'].set_color('gray')
ax2.spines['bottom'].set_color('gray')
ax2.tick_params(colors='gray')
ax2.grid(False)

plt.tight_layout()
plt.savefig('tufte_contrast.png', dpi=150)
plt.show()

tufte_contrast.png

预期输出: 左右两张图对比,左图默认样式有过多网格、灰色背景、四周框线,视觉杂乱;右图去掉上/右轴线、灰色简约风格、无网格,数据线的视觉比重明显提升。

实用建议(Tufte原则落实到代码中):

  • 去掉上框线和右框线:ax.spines['top'].set_visible(False) 和 ax.spines['right'].set_visible(False)
  • 网格线调低透明度(ax.grid(alpha=0.2))或只保留水平虚线
  • 数据标签直接标注在图内而非依赖图例,减少读者视线在图表和图例之间的来回移动
  • 颜色数量控制在 3-5 种以内,超出时使用灰度、线型或形状来辅助区分
  • 坐标轴刻度标签尽量简洁——0.001 写成 1×10⁻³,避免小数点过多
  • 非必要时不使用图例边框,通过 legend(frameon=False) 移除
  • Chartjunk 检查清单: 3D柱状图(除非必要)✗、图表内部填充渐变 ✗、元素阴影 ✗、装饰性背景图片 ✗、过于密集的网格线 ✗

一个简单的检查方法: 把你的图表截图,然后在图片编辑软件里把所有的"非数据元素"涂成白色。如果大量内容消失了,说明 data-ink ratio 太低,需要精简。

五、高级图表类型

5.1 小提琴图(Violinplot)替代箱线图 ​

小提琴图在箱线图的基础上增加了核密度估计(KDE),能展示数据分布的形状——这在数据存在多模态(多个峰)时尤其重要,箱线图完全无法揭示这种结构。

python
# 小提琴图 vs 箱线图:分布可见性对比
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))

# 准备三类数据
setosa_sepal = iris_df.iloc[:, 0][iris_df.species == 0]
versicolor_sepal = iris_df.iloc[:, 0][iris_df.species == 1]
virginica_sepal = iris_df.iloc[:, 0][iris_df.species == 2]
data_list = [setosa_sepal, versicolor_sepal, virginica_sepal]

# 左:箱线图(仅显示五数概括,看不到分布形态)
bp = ax1.boxplot(data_list, patch_artist=True,
                 boxprops=dict(facecolor='#85c1e9', alpha=0.6),
                 medianprops=dict(color='#1a5276', linewidth=2))
ax1.set_xticklabels(iris.target_names)
ax1.set_title('箱线图(仅显示五数概括)')
ax1.set_ylabel(iris.feature_names[0])

# 右:小提琴图(含KDE分布形状)
vp = ax2.violinplot(data_list, showmeans=True, showmedians=True)
colors_vp = ['#3498db', '#e74c3c', '#2ecc71']
for i, pc in enumerate(vp['bodies']):
    pc.set_facecolor(colors_vp[i])
    pc.set_alpha(0.7)
vp['cmeans'].set_color('darkred')       # 均值用深红色圆点
vp['cmedians'].set_color('darkblue')     # 中位数用蓝色短线
ax2.set_xticks([1, 2, 3])
ax2.set_xticklabels(iris.target_names)
ax2.set_title('QIAN DATA: 小提琴图(含分布形状)')
ax2.set_ylabel(iris.feature_names[0])

plt.tight_layout()
plt.savefig('violin_vs_box.png', dpi=200)
plt.show()

violin_vs_box.png

预期输出: 左侧箱线图展示五数概括(min、Q1、median、Q3、max),右侧小提琴图在此基础上叠加了核密度曲线。可以看到 setosa 的分布窄而集中(方差小),versicolor 和 virginica 有轻微右偏,箱线图完全无法暴露这些分布特征。

参数说明: showmeans=True 显示均值点,showmedians=True 显示中位数线;通过 vp['bodies'] 访问每个小提琴的主体部分,可单独设置颜色和透明度。

5.2 六边形分箱图(Hexbin)替代散点图 ​

当数据点密集(超过数千个)时,普通散点图会出现严重的重叠(overplotting),无法看出密度分布。Hexbin 将平面划分为六边形网格,用颜色深浅表示落在每个格子中的点数,本质上是二维直方图。

python
# hexbin 解决 overplotting — 大样本下的密度可视性
n = 10000
x_big = np.random.randn(n)
y_big = x_big * 0.5 + np.random.randn(n) * 0.3

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))

# 左:普通散点图(10000个点严重重叠,看不出密度差异)
ax1.scatter(x_big, y_big, alpha=0.3, s=5, c='#2980b9')
ax1.set_title(f'普通散点图 (n={n}) — overplotting严重')
ax1.set_xlabel('x'); ax1.set_ylabel('y')

# 右:六边形分箱图(密度分布一目了然)
hb = ax2.hexbin(x_big, y_big, gridsize=30, cmap='Blues',
                mincnt=1, edgecolors='white', linewidths=0.3)
ax2.set_title('QIAN DATA: 六边形分箱图 — 密度一目了然')
ax2.set_xlabel('x'); ax2.set_ylabel('y')
cb = plt.colorbar(hb, ax=ax2, shrink=0.8)
cb.set_label('频数')

plt.tight_layout()
plt.savefig('hexbin_demo.png', dpi=200)
plt.show()

hexbin_demo.png

预期输出: 左图蓝色点完全糊成一片,无法分辨哪里密度高;右图六边形格子的颜色从浅蓝到深蓝渐变,中间对角线区域密度最高(因为 x 和 y 相关),一目了然。

参数详解:

  • gridsize=30:控制六边形网格的疏密(每个方向划分的格数),越大格子越多
  • mincnt=1:最少有 1 个数据点的格子才显示,0 表示空网格也显示
  • cmap='Blues':用顺序型配色表达密度递增
  • edgecolors='white':格子边框颜色,让相邻网格视觉分离

5.3 配对图(Pairplot)替代多变量散点矩阵 ​

seaborn 的 pairplot 在探索性数据分析(EDA)中极为常用——它一次性展示所有变量之间的两两散点关系,对角线上显示单变量分布。

python
# 配对图(使用seaborn,即插即用)
iris_sample = iris_df.copy()
iris_sample['species_name'] = iris_sample['species'].map({
    0: 'setosa', 1: 'versicolor', 2: 'virginica'
})

sns.set_style('whitegrid')
g = sns.pairplot(iris_sample, hue='species_name',
                 palette='Set2',
                 diag_kind='kde',
                 plot_kws={'alpha': 0.6, 's': 30, 'edgecolor': 'white'},
                 diag_kws={'alpha': 0.7})
g.fig.suptitle('QIAN DATA: iris配对图(Pairplot — EDA利器)', y=1.02, fontsize=14)
plt.savefig('pairplot_iris.png', dpi=200, bbox_inches='tight')
plt.show()

pairplot_iris.png

预期输出: 一个 4×4 的散点矩阵,对角线是各特征的核密度估计曲线,非对角线是两两散点图,不同物种用不同颜色(Set2 配色)区分。可以清晰看到 setosa 在花瓣长度和宽度上与其他两类明显分离。

注意: pairplot 属于 seaborn 的高级 API,适合快速 EDA。若需精细控制每个子图的样式,可以用 matplotlib 配合 subplot_mosaic 或 GridSpec 实现自定义版本的散点矩阵。

六、主题自定义

6.1 rcParams 精细调参 ​

rcParams 是 matplotlib 的全局配置字典,修改它可以一次性地改变所有后续图表的样式。这是统一整个项目图表风格的最高效方式。

python
# rcParams 常见调参项
rcParams.update({
    'font.family': 'sans-serif',
    'font.size': 11,
    'axes.titlesize': 13,
    'axes.labelsize': 11,
    'axes.spines.top': False,      # 全局隐藏上框线
    'axes.spines.right': False,    # 全局隐藏右框线
    'axes.grid': True,
    'grid.alpha': 0.3,
    'grid.linestyle': '--',
    'lines.linewidth': 1.5,
    'lines.marker': '',            # 默认无标记点
    'figure.dpi': 150,             # 屏幕显示分辨率
    'savefig.dpi': 200,            # 文件保存分辨率
    'savefig.bbox': 'tight',       # 保存时自动裁剪空白
    'legend.frameon': False,       # 图例无边框
})

# 测试rcParams效果
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, y1, label='sin(x)+噪声')
ax.plot(x, y3, '--', label='sin·cos')
ax.set_xlabel('x'); ax.set_ylabel('y')
ax.set_title('QIAN DATA: rcParams 自定义风格')
ax.legend()
plt.savefig('rcparams_demo.png')
plt.show()

rcparams_demo.png

预期输出: 一张折线图,无上/右框线、灰色虚线网格、字体统一、标题略大于坐标轴标签、图例无边框,整体简洁干净。

常用 rcParams 速查表:

参数作用常见值
figure.figsize默认画布大小(8, 4) / (10, 5)
figure.dpi显示分辨率100 / 150
savefig.dpi保存分辨率200 / 300 / 600
font.family字体族'sans-serif' / 'SimHei'
axes.prop_cycle颜色循环器plt.cycler(color=[...])
lines.linewidth默认线宽1.5 / 2.0
legend.loc图例位置'best' / 'upper right'
axes.unicode_minus负号显示True(解决中文下负号显示为方块)

6.2 Seaborn 风格对比 ​

seaborn 提供了开箱即用的美观主题,可以在不修改 rcParams 的前提下快速切换整个图表的外观。

python
# 不同seaborn主题对比(独立出图,各展示一张)
styles = ['whitegrid', 'darkgrid', 'ticks', 'talk']

for i, style in enumerate(styles):
    fig, ax = plt.subplots(figsize=(6, 3))
    sns.set_style(style)
    ax.plot(x, y1, color='#2980b9', linewidth=1.5, label='sin(x)')
    ax.plot(x, y3, '--', color='#e74c3c', linewidth=1.2, label='sin·cos')
    ax.set_title(f'QIAN DATA: seaborn style="{style}"')
    ax.set_xlabel('x'); ax.set_ylabel('y')
    ax.legend()
    plt.tight_layout()
    plt.savefig(f'style_{style}.png', dpi=150)
    plt.show()

# 恢复常用风格
sns.set_style('whitegrid')

预期输出: 四张主题不同的折线图——

主题效果
whitegrid白色背景 + 灰色网格线,最通用
darkgrid深灰色背景 + 白色网格线,适合暗色模式
ticks白色背景 + 刻度线,无网格线
talk大字号,适合演讲投影

6.3 自定义颜色循环 ​

matplotlib 默认的颜色循环(tab10)在超过 10 个类别时会重复使用,且默认配色偏亮。自定义颜色循环可以保证整个文档/项目的配色一致性。

python
# 自定义颜色循环:八色QIAN配色
QIAN_COLORS = ['#2980b9', '#e74c3c', '#2ecc71', '#f39c12',
               '#9b59b6', '#1abc9c', '#e67e22', '#3498db']
rcParams['axes.prop_cycle'] = plt.cycler(color=QIAN_COLORS)

# 测试:画8条曲线,自动循环使用QIAN颜色
fig, ax = plt.subplots(figsize=(8, 4))
for i in range(8):
    ax.plot(x, np.sin(x + i * np.pi/4) + i*0.5,
            linewidth=1.5, label=f'系列{i+1}')
ax.set_title('QIAN DATA: 自定义颜色循环')
ax.legend(ncol=4, fontsize=8)
plt.tight_layout()
plt.savefig('custom_cycle.png', dpi=150)
plt.show()

custom_cycle.png

预期输出: 八条曲线依次使用 QIAN_COLORS 中的八个颜色(蓝、红、绿、橙、紫、青、棕、亮蓝),各条曲线清晰区分,无需手动为每条曲线指定颜色。

技巧: 将 QIAN_COLORS 定义在项目的配置文件中,所有图表统一导入,保证整篇文章的视觉连续性。

七、多子图布局

7.1 subplot_mosaic 布局 ​

matplotlib 3.3+ 引入了 subplot_mosaic,可以用类 ASCII 字符串的方式定义复杂布局,比传统 GridSpec 更直观。

python
# subplot_mosaic 语法演示
mosaic = """
    AAB
    CCD
    EED
"""
fig, axes = plt.subplot_mosaic(mosaic, figsize=(10, 8),
                               per_subplot_kw={'D': {'projection': 'polar'}})

# 填充各子图
axes['A'].plot(x, y1, color='#2980b9')
axes['A'].set_title('QIAN DATA: 子图A - 折线图')

axes['B'].bar(['A','B','C','D'], [12,35,28,45], color='#2ecc71')
axes['B'].set_title('子图B - 柱状图')

axes['C'].hist(y1, bins=20, color='#f39c12', edgecolor='white')
axes['C'].set_title('子图C - 直方图')

axes['D'].plot(np.linspace(0, 2*np.pi, 100),
               np.abs(np.sin(np.linspace(0, 2*np.pi, 100))), color='#e74c3c')
axes['D'].set_title('子图D - 极坐标')

axes['E'].scatter(y1, y2, c='#9b59b6', alpha=0.6, s=20)
axes['E'].set_title('子图E - 散点图')

plt.tight_layout()
plt.savefig('mosaic_layout.png', dpi=200)
plt.show()

mosaic_layout.png

预期输出: 一张 3×3 网格的复杂布局图——

A A B      ← A跨两列,B占一列
C C D      ← C跨两列,D是极坐标投影(右侧一列)
E E D      ← E跨两列,D从第二行延伸到第三行

mosaic 核心优势:

  • 用字符矩阵直观表达布局,代码可读性极高
  • 重复字符 = 跨行/跨列,同一字符在矩阵中出现多次表示合并单元格
  • per_subplot_kw 支持为特定子图单独设置投影(如 polar、3D)等参数
  • 返回字典(axes['A']),通过键名访问,比索引更直观

7.2 GridSpec 高级用法 ​

GridSpec 提供更精细的尺寸比例控制,适合需要非均匀宽度/高度的复杂面板场景。

python
# GridSpec 高级:自定义宽高比
fig = plt.figure(figsize=(12, 6))
gs = GridSpec(2, 3, figure=fig,
              width_ratios=[2, 1, 1],   # 第一列宽度是后两列的2倍
              height_ratios=[1, 1.5],   # 第二行高度是第1行的1.5倍
              hspace=0.3, wspace=0.3)

# 左上(宽列占2倍宽度)
ax1 = fig.add_subplot(gs[0, 0])
ax1.plot(x, y1, color='#2980b9', linewidth=2, label='sin(x)')
ax1.plot(x, y3, '--', color='#e74c3c', linewidth=1.5, label='sin·cos')
ax1.set_title('QIAN DATA: 主曲线图')
ax1.legend(fontsize=9)
ax1.grid(alpha=0.3)

# 右上
ax2 = fig.add_subplot(gs[0, 1])
ax2.scatter(y1, y2, c='#2980b9', alpha=0.6, s=20, edgecolors='white')
ax2.set_title('散点图')

# 右中
ax3 = fig.add_subplot(gs[0, 2])
ax3.hist(y1, bins=20, color='#85c1e9', edgecolor='#2980b9', alpha=0.7)
ax3.set_title('直方图')

# 底部(跨越整行,gs[1, :] 表示第二行所有列)
ax4 = fig.add_subplot(gs[1, :])
ax4.fill_between(x, y1, alpha=0.3, color='#85c1e9')
ax4.plot(x, y1, color='#2980b9', linewidth=1.5)
ax4.set_title('底部宽图 - 填充面积图')
ax4.set_xlabel('x'); ax4.set_ylabel('y')

plt.savefig('gridspec_advanced.png', dpi=200, bbox_inches='tight')
plt.show()

gridspec_advanced.png

预期输出: 上排三张图——主折线图占两倍宽度(左侧)、散点图和直方图各占一份(右侧),底部一张跨越整行的填充面积图。整体形成"上三下一"的排版。

GridSpec 参数详解:

  • width_ratios:各列的宽度比例,[2,1,1] 表示第一列宽度是后两列之和
  • height_ratios:各行的高度比例
  • hspace / wspace:子图之间的水平和垂直间距(单位为子图尺寸的比例)
  • gs[行, 列]:支持切片语法,gs[0, :] 表示第一行全部列,gs[1, 0:2] 表示第二行前两列

八、双Y轴

当两个序列量纲不同(如营收和增长率)或数值范围差异极大时,双Y轴可以让它们在同一张图中展示对比关系。

python
# 双Y轴:营收(万元)与增长率(%)同图展示
months = np.arange(1, 13)
revenue = np.array([12, 15, 18, 22, 25, 30, 35, 33, 28, 26, 20, 18])
growth_rate = np.array([8, 6, 5, 4, 3, 2, 1, 0, -2, -4, -5, -6])

fig, ax1 = plt.subplots(figsize=(8, 4))

color1 = '#2980b9'   # 左侧Y轴颜色(柱状图)
color2 = '#e74c3c'   # 右侧Y轴颜色(折线图)

ax1.bar(months, revenue, color=color1, alpha=0.7, label='营收(万元)')
ax1.set_xlabel('月份')
ax1.set_ylabel('营收(万元)', color=color1)
ax1.tick_params(axis='y', labelcolor=color1)
ax1.set_xticks(months)

ax2 = ax1.twinx()    # 创建共享X轴的第二Y轴
ax2.plot(months, growth_rate, color=color2, linewidth=2, marker='o', label='增长率(%)')
ax2.set_ylabel('增长率(%)', color=color2)
ax2.tick_params(axis='y', labelcolor=color2)
ax2.axhline(y=0, color='gray', linestyle='--', linewidth=0.5)  # 零值参考线

plt.title('QIAN DATA: 双Y轴 - 营收与增长率 - ' + today)
fig.tight_layout()
plt.savefig('dual_axis.png', dpi=200)
plt.show()

dual_axis.png

预期输出: 柱状图(营收,左侧Y轴,蓝色系)和折线图(增长率,右侧Y轴,红色系)叠加显示。增长率从第7个月开始降至0以下,转为负增长,与营收的下降趋势吻合。

使用注意: 双Y轴容易误导读者的视觉判断——左侧的柱峰和右侧的线峰可能在同一位置但数值完全不同。建议在图例中明确标注两组数据的分属坐标轴,且两个轴的范围尽量设计为视觉可比(如让两者的"视觉高峰"对齐)。

九、热力图与相关性矩阵

热力图是展示特征间相关性的标准手段,配合上三角掩码(mask)可避免冗余展示对称矩阵的两侧。

python
# 热力图 + 上三角掩码(相关性矩阵)
corr = iris_df.iloc[:, :4].corr()
mask = np.triu(np.ones_like(corr, dtype=bool))  # 上三角为True

plt.figure(figsize=(8, 6))
sns.heatmap(corr, mask=mask, annot=True, fmt='.3f',
            cmap='RdBu', center=0, square=True,
            linewidths=0.5, cbar_kws={'shrink': 0.8},
            vmin=-1, vmax=1)
plt.title('QIAN DATA: 特征相关性热力图 - ' + today)
plt.tight_layout()
plt.savefig('heatmap.png', dpi=200)
plt.show()

heatmap.png

预期输出: 一张下三角显示的热力图,相关系数保留3位小数标注在格子中。RdBu配色——红色表示正相关,蓝色表示负相关,白色表示零相关。petal length(花瓣长度)和 petal width(花瓣宽度)之间的相关系数接近 0.96,强正相关。

参数说明:

  • mask=np.triu(...):遮盖上三角区域(因相关系数矩阵对称),避免重复展示
  • annot=True, fmt='.3f':在格子中标注数值,保留三位小数
  • center=0:以零值为中点,正负相关性用不同颜色方向表达
  • vmin=-1, vmax=1:固定颜色映射范围到 [-1, 1]
  • square=True:每个格子为正方形
  • linewidths=0.5:格子之间的分割线宽度

十、输出优化

10.1 不同格式的输出参数 ​

matplotlib 支持多种矢量图和位图格式,不同使用场景应选择不同输出格式:

格式后缀适用场景特点
位图PNG网页、公众号通用性最强,不支持无限缩放
矢量图SVG网页、矢量编辑器(如Illustrator)无限缩放,文件较小,可编辑
矢量图EPSLaTeX 论文插入学术出版标准,适合印刷
矢量图PDF文档排版,多图合成支持嵌入字体,跨平台兼容性好
python
# 输出格式对比:同时保存四种格式
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, y1, color='#2980b9', linewidth=1.5, label='sin(x)+噪声')
ax.plot(x, y3, '--', color='#e74c3c', linewidth=1.2, label='sin·cos')
ax.set_title('QIAN DATA: 输出格式对比')
ax.set_xlabel('x'); ax.set_ylabel('y')
ax.legend()

# 同时保存多种格式
plt.savefig('output_demo.png', dpi=300, bbox_inches='tight')   # 位图,300dpi
plt.savefig('output_demo.svg', bbox_inches='tight')            # 矢量图
plt.savefig('output_demo.pdf', bbox_inches='tight')            # 矢量图
plt.savefig('output_demo.eps', bbox_inches='tight')            # 矢量图
plt.show()

output_demo.png

预期输出: 四份相同内容的文件。用浏览器打开 SVG 可无限缩放不失真,PDF 插入 LaTeX 编译后保持清晰,PNG(300dpi)在任何查看器中都正常显示但文件最大。

10.2 DPI 选择与字体嵌入 ​

DPI(dots per inch,每英寸点数)直接影响输出图片的清晰度和文件大小。

python
# dpi 对清晰度的影响对比
fig, axes = plt.subplots(1, 2, figsize=(10, 4))

x_test = np.linspace(0, 2*np.pi, 50)
for ax, dpi_val in zip(axes, [72, 300]):
    ax.plot(x_test, np.sin(x_test), 'o-', markersize=4)
    ax.set_title(f'QIAN DATA: dpi={dpi_val}')
    ax.set_xlabel('x'); ax.set_ylabel('sin(x)')

plt.tight_layout()
plt.savefig('dpi_72.png', dpi=72, bbox_inches='tight')
plt.savefig('dpi_300.png', dpi=300, bbox_inches='tight')
plt.show()

dpi_72.png

dpi_300.png

预期输出: 两张看似相同的图,但 dpi=72 的图片在放大时出现锯齿(马赛克边缘),dpi=300 的图片边缘平滑。将两张图片插入 Word 并缩放对比时差异更为明显。

DPI 选择指南:

场景推荐 DPI说明
公众号 / 网页72-150屏幕分辨率足够,文件小加载快
一般打印 / Word200-300折中清晰度和文件大小
学术论文 / 出版印刷300-600印刷品要求最高清晰度
仅屏幕查阅100节省空间

字体嵌入(PDF/EPS 输出):

python
# 确保输出中的字体正确嵌入(避免PDF在其他电脑上字体缺失)
rcParams['pdf.fonttype'] = 42       # Type 42(TrueType),保真度最高
rcParams['ps.fonttype'] = 42        # PostScript / EPS 字体类型
rcParams['font.family'] = 'sans-serif'

fonttype=42 将 TrueType 字体嵌入为可缩放路径,避免 PDF 在不同操作系统上字体替换导致的排版错乱。

十一、自定义配色方案

一套好的配色方案是学术图表的"门面",也是形成个人/团队视觉品牌的有效手段。

python
# QIAN蓝色系配色方案
QIAN_BLUE = ['#e8f4fd', '#b3d9f2', '#85c1e9', '#59b3f9',
             '#2980b9', '#1a5276', '#0e2f5a']

categories = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
values = [12, 35, 28, 45, 18, 33, 22]

plt.figure(figsize=(8, 4))
bars = plt.bar(categories, values, color=QIAN_BLUE, edgecolor='white')
plt.xlabel('类别')
plt.ylabel('值')
plt.title('QIAN DATA: 蓝色系配色方案')

# 数据标签标注
for bar, val in zip(bars, values):
    plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
             str(val), ha='center', fontsize=10)

plt.tight_layout()
plt.savefig('custom_colors.png', dpi=200)
plt.show()

custom_colors.png

预期输出: 七根柱状图从浅蓝到深蓝渐变,每个柱的上方标注对应的数值,整体风格统一、层次分明。

QIAN色板(推荐复制到项目中统一使用):

名称色值用途
QIAN浅蓝#e8f4fd背景填充、次要区域
QIAN中浅蓝#b3d9f2辅助数据系列
QIAN中蓝#85c1e9次要数据系列
QIAN强调蓝#59b3f9装饰线、辅助元素
QIAN主色蓝#2980b9主数据系列、主线条
QIAN深蓝#1a5276主标题、主轴线
QIAN极深蓝#0e2f5a强调文本、背景极深色

十二、常见痛点(附解决方案)

12.1 中文乱码 ​

这是 matplotlib 中文用户遇到最多、也最容易让人崩溃的坑。核心原因是 matplotlib 默认字体(DejaVu Sans)中不包含中文字符集(CJK Unified Ideographs),因此遇到中文就直接显示为一个个小方格(□□□□)。更令人困惑的是,有时候同样的代码在同事的电脑上正常显示,在自己机器上就乱码,这是因为两台机器上安装的字体不同。

python
# 标准解决方案:指定可用的中文字体
rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei']
rcParams['axes.unicode_minus'] = False  # 解决负号显示为方块的问题(重要!)

为什么 axes.unicode_minus 必须设为 False? 因为 matplotlib 默认使用 Unicode 的减号字符(U+2212)来渲染负号,但某些中文字体不包含这个字符,导致 -1 显示为方块。设为 False 后改用标准的短横线(连字符)代替。

python
# 如果不知道系统安装了哪些中文字体,运行以下代码查看
import matplotlib.font_manager as fm
fonts_available = [f.name for f in fm.fontManager.ttflist]
chinese_fonts = [f for f in fonts_available if any(k in f for k in ['Hei', 'YaHei', 'Song', 'Fang', 'Kai', 'Ming', 'CJK', 'Noto'])]
print('可用的中文字体:', chinese_fonts)
# 输出示例:['SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei', 'Noto Sans CJK SC']

各平台推荐中文字体:

平台推荐字体fallback
WindowsSimHei(黑体)、Microsoft YaHei(微软雅黑)DengXian
macOSPingFang SC、Heiti SCSTHeiti
LinuxWenQuanYi Micro Hei、Noto Sans CJK SC—

12.2 图例重叠 ​

当图例条目过多(超过5条)或图表尺寸偏小时,图例会与数据区域重叠,遮挡信息。这在多曲线对比图中尤其常见。

python
# 图例重叠的三种解决方案
fig, ax = plt.subplots(figsize=(8, 4))
for i in range(6):
    ax.plot(x, np.sin(x + i*0.5) + i*0.3, label=f'曲线{i+1}')

# 方案A(推荐):放在绘图区域右侧外部,完全不遮挡数据
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0)

# 方案B:多列排列——适合条目较多时横向排布
# ax.legend(ncol=2, fontsize=8)

# 方案C:调整位置到角落——适合条目较少时
# ax.legend(loc='lower right')

# 方案D:将图例放在图外下方
# ax.legend(bbox_to_anchor=(0.5, -0.15), loc='upper center', ncol=3)

ax.set_title('QIAN DATA: 图例放置优化')
plt.tight_layout()
plt.savefig('legend_fix.png', dpi=150, bbox_inches='tight')
plt.show()

legend_fix.png

预期输出: 图例放在绘图区域右侧外部(bbox_to_anchor=(1.05, 1)),不遮挡任何数据曲线。

12.3 坐标轴标签截断 ​

长标签(尤其是中文标签或较长的变量名)经常在保存时被裁剪掉一部分。

python
# 标签截断的标准解决方案
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, y1)
ax.set_xlabel('这是一个很长的横坐标标签,默认会被截断')
ax.set_ylabel('同样很长的纵坐标标签')

# 方案1:tight_layout 自动调整
plt.tight_layout()
plt.savefig('label_cutoff.png', dpi=150, bbox_inches='tight')

# 方案2(备用):手动调整边距
# fig.subplots_adjust(bottom=0.15, left=0.15)
plt.show()

label_cutoff.png

预期输出: 长标签完整显示,没有被截断或覆盖。tight_layout() 自动压缩各元素的间距,bbox_inches='tight' 在保存时进一步裁剪画布边界。

12.4 保存模糊 ​

保存的图片在 Word 或 PPT 中放大后出现锯齿、模糊——这是科研论文中最常被审稿人批评的问题之一。

python
# 保存模糊的根本原因和对应策略
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, y1, linewidth=1.5)
ax.set_title('QIAN DATA: 清晰度测试')

# 错误做法:使用默认低dpi
plt.savefig('fuzzy.png', dpi=72)                 # ❌ 模糊!不适用于任何正式用途

# 正确做法:高dpi + 裁剪空白
plt.savefig('sharp.png', dpi=300, bbox_inches='tight')  # ✅ 清晰,适合投稿

# 终极方案:使用矢量格式
plt.savefig('sharp.svg', bbox_inches='tight')     # ✅ 无限缩放,永不模糊
plt.show()

sharp.png

fuzzy.png

清晰度检查清单(投稿前逐项确认):

  • [ ] savefig(dpi=300) — dpi 是否至少300
  • [ ] bbox_inches='tight' — 是否裁剪多余空白
  • [ ] 矢量格式(SVG/PDF/EPS)— 如果期刊支持矢量图,优先使用
  • [ ] 字体大小 — 缩放后的字体是否仍可读(建议字号 ≥ 8pt)
  • [ ] 线条粗细 — 缩小后线条是否可见(建议 ≥ 0.5pt)
  • [ ] 颜色打印兼容性 — 是否在灰度打印下也能区分

总结

本文从 matplotlib 的三层架构出发,系统介绍了高级绘图的各个环节,涵盖了从数据可视化原理到实际代码实现的完整链条。

主题工具 / 方法适用场景
颜色映射顺序型 / 发散型 / 定性型不同类型数据的配色选择
排版优化Tufte原则、data-ink ratio学术论文级图表制作
高级图表violinplot / hexbin / pairplot大数据、多变量、分布展示
主题自定义rcParams / seaborn风格统一整个项目的图表风格
多子图布局subplot_mosaic / GridSpec复杂信息面板设计
输出优化SVG/EPS/PDF / dpi选择 / 字体嵌入出版印刷、网页展示
常见痛点中文乱码 / 图例重叠 / 标签截断 / 保存模糊日常开发避坑指南

记住:好的可视化不是画得花哨,而是在最小的空间里传递最多的信息(max data-ink ratio)。 每一根线条、每一个颜色、每一处标注,都应该为传递数据服务。

参考资源

[1] matplotlib 官方文档. https://matplotlib.org/stable/tutorials/. [2] seaborn 官方教程. https://seaborn.pydata.org/tutorial.html. [3] Tufte, E. (2001) The Visual Display of Quantitative Information, 2nd ed [4] matplotlib colormap 参考:https://matplotlib.org/stable/users/explain/colors/colormaps.html.

关注公众号:QIAN数据