📊 第5章 可视化数据分析图表
📄 一句话概括

3D 图要 projection='3d',这个参数漏了会报错说没有 plot_surface。 subplots 一次拿回一堆 axes,用 axes[行, 列] 取;subplot 编号从 1 起、先行后列。 dpi 从 100 到 300,字节涨 3.7 倍。印刷才需要 300,屏幕 100 够。

📝 本章和前面几章的关系

前面十篇都是一张纸一块图。这一篇讲一张纸贴好几块,还有三维曲面。 多子图是「一屏说清一件事」的唯一办法,比存五张图挨个发有效得多。

一、3D 曲面

np.meshgrid 造 40 乘 40 的网格,高度取 sin(到原点的距离),得到一圈圈水波纹。 fig.add_subplot(111, projection='3d') 拿到一块 3D 的 axes。 projection='3d' 漏了会报 AttributeError,说没有 plot_surface 这个方法。

3D 图好看但难读数:同一个高度,从不同角度看位置完全不同。 能压成二维热力图就压,热力图读数准得多。3D 适合看形状,不适合报数。

二、多子图:subplots 一次拿一堆

fig, axes = plt.subplots(2, 2, figsize=(8, 6)) 拿到 2 乘 2 的数组。 用 axes[0, 0]、axes[0, 1]、axes[1, 0]、axes[1, 1] 逐块画。 四张图一张纸 34229 字节,比四张单独的图加起来小得多。

一张纸贴多张图时一定要 tight_layout,不然标题和标签互相压。

三、subplot 与 subplots 的区别

写法 给什么 怎么取
subplots(2, 2) 一次给整个数组 axes[行, 列],从 0 起
subplot(2, 2, 1) 一个一个加 编号从 1 起,先行后列

subplots 省事,subplot 灵活(比如只画 2 乘 2 布局里的第 1 块和第 4 块)。 subplot 的编号从 1 开始,这一点和 Python 别处的 0 起不一样,容易记混。

四、保存:格式与 dpi

同样一张 6 乘 4 的图:dpi=100 是 17293 字节,dpi=300 是 63367 字节,差 3.7 倍。 理论上面积差 9 倍,压缩之后实际只到 3.7 倍。 savefig 按扩展名决定格式:.png 位图、.pdf 矢量、.svg 矢量。

论文和报告用 pdf 或 svg,放多大都不糊。png 放大就出马赛克。

五、动手 Demo:3D 图、子图与保存

画一张 3D 曲面,再画一张 2 乘 2 四子图,最后对比 dpi 100 和 300 的字节数。 四子图里故意混四种图,看布局能不能撑住。

import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np

plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False

X = np.linspace(-3, 3, 40)
Y = np.linspace(-3, 3, 40)
XX, YY = np.meshgrid(X, Y)
ZZ = np.sin(np.sqrt(XX ** 2 + YY ** 2))
fig = plt.figure(figsize=(6, 5))
ax = fig.add_subplot(111, projection="3d")
ax.plot_surface(XX, YY, ZZ, cmap="viridis")
fig.savefig("surf.png")

YW = [88, 92, 75, 85, 90, 68, 95, 72, 83, 79]
fig, axes = plt.subplots(2, 2, figsize=(8, 6))
axes[0, 0].plot(range(6), [128, 116, 143, 151, 138, 162])
axes[0, 0].set_title("折线")
axes[0, 1].bar(["一", "二", "三"], [3, 1, 2])
axes[0, 1].set_title("柱形")
axes[1, 0].hist(YW, bins=5)
axes[1, 0].set_title("直方")
axes[1, 1].pie([5, 3, 2], labels=["A", "B", "C"])
axes[1, 1].set_title("饼图")
fig.tight_layout()
fig.savefig("subplots.png")
print("四子图 %d 字节" % os.path.getsize("subplots.png"))

for d in (100, 300):
    f, a = plt.subplots(figsize=(6, 4))
    a.plot(range(6), [128, 116, 143, 151, 138, 162])
    f.savefig("dpi%d.png" % d, dpi=d)
    print("dpi=%3d -> %d 字节" % (d, os.path.getsize("dpi%d.png" % d)))
    plt.close(f)

实跑输出:

--------------------------------------------------------------------
3D 图、子图与保存
--------------------------------------------------------------------

  ① 3D 曲面
  --------------------------------------------------------------------
  40 乘 40 的网格,曲面的高度是 sin(到原点的距离)——一个水波纹。
  3D 图要 projection='3d',这个参数漏了会报错说没有 plot_surface。
  ! 3D 图好看但难读数。能压成二维热力图就压,热力图读数准得多。

  ② 多个子图:subplots
  --------------------------------------------------------------------
  subplots(2, 2) 一次拿到 2 乘 2 的 axes 数组,用 axes[行, 列] 取。
  四张图一张纸,字节数 34229——比四张单独的图加起来小得多。
  ! 一张纸贴多张图时一定要 tight_layout。不然标题和标签互相压。

  ③ 单张索引写法:subplot
  --------------------------------------------------------------------
  add_subplot(2, 2, 1) 的意思是「2 行 2 列的第 1 个」,编号从 1 起、先行后列。
  subplots 一次给一堆,subplot 一个一个加。前者省事,后者灵活。
  ! 编号从 1 开始,这一点和 Python 其他地方的 0 起不一样,容易记混。

  ④ 保存:格式与 dpi
  --------------------------------------------------------------------
  同样一张图,dpi=100 是 17293 字节,dpi=300 是 63367 字节,差 3.7 倍。
  dpi 是每英寸点数。300 是印刷标准,屏幕上看 100 就够。
  savefig 按扩展名决定格式:.png 位图、.pdf 矢量、.svg 矢量。
  ! 论文和报告用 pdf 或 svg,放多大都不糊。png 放大就出马赛克。

np.meshgrid 把两个一维坐标铺成两张二维网格,曲面图必须这么给。 四子图里 pie 没有 x 轴 y 轴,所以它那块不会显示刻度,其余三块都有。

30 秒实验

  1. 把 projection='3d' 去掉,看报什么错。
  2. 把 subplots(2, 2) 改成 (3, 1),重新分配四块图。
  3. 想一想:为什么矢量格式(pdf、svg)放大不糊,png 会糊?

常见误区

  1. 3D 图漏了 projection:报没有 plot_surface。
  2. subplot 编号当成 0 起:布局乱掉,或者直接报错。
  3. 多子图不 tight_layout:标题互相压,坐标轴叠在一起。
  4. 屏幕图也用 dpi=300:文件大十倍,肉眼看不出区别。
自测三问(点开对答案) 1. subplots 和 subplot 有什么区别? —— subplots 一次返回整个 axes 数组(0 起索引),subplot 一次加一块(编号 1 起)。 2. 为什么四子图比四张单独的图小? —— 共用一张画布,坐标轴、字体、配色表这些公共部分只存一份。 3. 什么时候该用 pdf 而不是 png? —— 要打印、要放大查看、要放进报告排版时。pdf 是矢量,放多大都不糊。

最后一篇换工具:Seaborn。一行一个参数就换风格,还能直接吃 DataFrame。

相关笔记