DATA NOTEBOOK阅读地图
12个主要章节
11段代码示例
6组图片与图表

先确认变量、单位与假设,再核对手算、图表和解释范围。

章节速览12 节 · 展开选择

数据可视化不只是“把图画出来”。一张合格的图需要回答三个问题:比较的对象是什么、变化或差异在哪里、读者应该得出什么结论。本文从 Matplotlib 的对象模型开始,用一组无需外部数据文件的示例,建立从选图到导出的完整工作流。

一、先理解 Figure、Axes 与 Artist

Matplotlib 图形可以用三层结构理解:

  • Figure:整张画布,负责整体尺寸、布局和导出;
  • Axes:画布中的一个绘图区,包含坐标轴、标题和刻度;
  • Artist:图上的具体元素,例如折线、散点、文字和图例。

日常绘图推荐从 plt.subplots() 获得 Figure 和 Axes,再调用 ax.plot()、ax.scatter() 等方法。相比把所有状态交给 pyplot,这种对象式写法在多子图和函数封装中更清晰。

二、根据问题选择图表

想回答的问题 推荐图表 常见误区
数值如何随时间或顺序变化 折线图 横轴没有自然顺序却强行连线
两个数值变量是否相关 散点图 点重叠严重但没有透明度
不同类别谁高谁低 柱状图 从非零基线开始夸大差异
连续变量如何分布 直方图 只看一个分箱数就下结论
多组分布和离群值有何差异 箱线图 把离群点直接当作错误数据
整体由少量部分如何构成 饼图 类别过多或数值非常接近

饼图只适合类别少、总和有明确意义的构成关系。需要精确比较时,排序后的条形图通常更易读。

选图实验

先选问题,再选图表

同一组数据可以画成很多图,但图表必须服从你要回答的问题。

三、创建第一张图

导入绘图模块

1
import matplotlib.pyplot as plt

使用对象式接口

1
2
3
4
5
6
7
8
9
10
11
12
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2 * np.pi, 200)

fig, ax = plt.subplots(figsize=(8, 4), layout="constrained")
ax.plot(x, np.sin(x), label="sin(x)", linewidth=2)
ax.set(title="正弦函数", xlabel="x(弧度)", ylabel="y")
ax.grid(alpha=0.25)
ax.legend()

plt.show()

layout="constrained" 会为标题、坐标标签和图例自动预留空间,适合大多数常规布局。

正弦函数在零到二倍圆周率间的折线,图中含标题、弧度横轴、纵轴和图例
对象式接口的实际输出。 运行上方完整代码,得到200个采样点的正弦曲线;这是函数绘图,不是观测数据。查看原图

创建子图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2 * np.pi, 300)
fig, axes = plt.subplots(
nrows=2,
ncols=1,
figsize=(8, 6),
sharex=True,
layout="constrained",
)

axes[0].plot(x, np.sin(x), color="#4c78a8")
axes[0].set(title="sin(x)", ylabel="振幅")
axes[1].plot(x, np.cos(x), color="#e45756")
axes[1].set(title="cos(x)", xlabel="x(弧度)", ylabel="振幅")

for ax in axes:
ax.grid(alpha=0.2)

plt.show()

避免用 plt.subplot(221) 这类紧凑但不直观的数字写法。显式写出行列数,更容易调整布局。

中文字体与负号

Matplotlib 是否能显示中文取决于系统是否安装对应字体。不要把本机字体文件路径硬编码进可分享的代码,可以提供一组候选字体:

1
2
3
4
5
6
7
8
9
import matplotlib.pyplot as plt

plt.rcParams["font.sans-serif"] = [
"Microsoft YaHei",
"SimHei",
"Noto Sans CJK SC",
"Arial Unicode MS",
]
plt.rcParams["axes.unicode_minus"] = False

四、散点图:观察关系与异常点

若要把多变量压到两个坐标,可结合 PCA 的方差方向与重构误差阅读:投影图需要说明中心化、特征尺度和保留维数,二维上靠近也不能直接代表原空间中的全部关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(42)
study_hours = rng.uniform(0.5, 8, 80)
score = 52 + 5.5 * study_hours + rng.normal(0, 7, 80)

fig, ax = plt.subplots(figsize=(7, 4), layout="constrained")
ax.scatter(
study_hours,
score,
s=45,
alpha=0.7,
color="#4c78a8",
edgecolors="white",
linewidths=0.5,
)
ax.set(title="学习时长与成绩", xlabel="每天学习时长(小时)", ylabel="成绩")
ax.grid(alpha=0.2)

plt.show()

alpha 可以减轻点重叠造成的遮挡。需要表达第三个连续变量时,可以把它映射到点大小或颜色,但要补充图例或颜色条。

固定种子42生成的80个学习时长与成绩散点,呈带随机扰动的上升趋势
散点展示的是关系与离散程度。 由上方代码、种子42生成80个合成样本;不是学生调查,不支持关于学习时间的真实因果结论。查看原图

五、折线图:表达连续变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import matplotlib.pyplot as plt

hours = list(range(0, 24, 2))
temperature = [15, 14, 14, 16, 20, 24, 27, 29, 28, 25, 21, 18]

fig, ax = plt.subplots(figsize=(8, 4), layout="constrained")
ax.plot(
hours,
temperature,
marker="o",
color="#e45756",
linewidth=2,
label="气温",
)
ax.set(
title="一天中的气温变化",
xlabel="时刻",
ylabel="温度(℃)",
xticks=hours,
)
ax.grid(axis="y", alpha=0.25)
ax.legend()

plt.show()

如果有多条线,应通过颜色之外的线型或标记再增加一种区分方式,避免颜色辨识困难或黑白打印后无法判断。

教学气温序列每两小时一个点,从15摄氏度下降后升至29摄氏度,再回落到18摄氏度
有自然顺序的数据才适合连线。 图使用正文给定的12个教学数值;不是某城市的天气记录。线段连接相邻采样点,不表示已测得每个中间时刻的温度。查看原图

六、柱状图:比较离散类别

柱状图通常应从零开始,因为读者会通过柱子的长度判断大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import matplotlib.pyplot as plt

languages = ["Python", "C++", "Java", "Go"]
solved = [42, 31, 24, 18]

fig, ax = plt.subplots(figsize=(7, 4), layout="constrained")
bars = ax.bar(languages, solved, color="#72b7b2")
ax.bar_label(bars, padding=3)
ax.set(
title="不同语言完成的练习数",
xlabel="语言",
ylabel="题目数",
ylim=(0, 50),
)
ax.spines[["top", "right"]].set_visible(False)

plt.show()

当类别名称很长时,可使用 ax.barh() 绘制水平条形图,并把类别按数值排序。

Python、C++、Java和Go的示例练习数分别为42、31、24、18,柱状图从零开始
类别比较保留零基线。 四根柱来自正文的合成练习数,柱顶显示数值;不代表语言流行度或效率排名。查看原图

七、直方图:查看连续变量的分布

直方图把连续数值划入若干区间。分箱太少会隐藏结构,太多则会放大随机波动,因此应尝试几种分箱方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(7)
response_time = rng.lognormal(mean=4.5, sigma=0.35, size=1000)

fig, ax = plt.subplots(figsize=(7, 4), layout="constrained")
ax.hist(
response_time,
bins="fd", # Freedman-Diaconis 规则
color="#f2cf5b",
edgecolor="white",
)
ax.set(title="接口响应时间分布", xlabel="响应时间(ms)", ylabel="请求数")

plt.show()

直方图回答“数据落在哪些区间”,而柱状图回答“每个离散类别是多少”。两者外观相似,但含义不同。

固定种子7生成的1000个对数正态响应时间,使用Freedman-Diaconis规则分箱,分布右侧有长尾
分布图保留单位与分箱口径。 使用上方种子7、1000个合成样本和 bins="fd";不是实际接口压测结果。尝试其他分箱后再判断结构。查看原图

八、箱线图:比较多组分布

箱体从第一四分位数延伸到第三四分位数,中线表示中位数;默认“须”覆盖距离箱体不超过 1.5 × IQR 的数据点,更远的点作为离群点单独绘制。离群点提示需要检查,不代表它一定无效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(21)
groups = [
rng.normal(72, 8, 60),
rng.normal(78, 6, 60),
rng.normal(75, 10, 60),
]

fig, ax = plt.subplots(figsize=(7, 4), layout="constrained")
result = ax.boxplot(
groups,
tick_labels=["方案 A", "方案 B", "方案 C"],
patch_artist=True,
showmeans=True,
)

for box, color in zip(result["boxes"], ["#9ecae9", "#a1d99b", "#fdae6b"]):
box.set_facecolor(color)

ax.set(title="三种方案的得分分布", ylabel="得分")
ax.grid(axis="y", alpha=0.2)

plt.show()

Matplotlib 3.9 将箱线图的 labels 参数改名为 tick_labels,并在 3.11 移除了旧名称。新代码应使用 tick_labels。

九、导出适合发布的图片

保存图片时应保留 fig 对象,并在 show() 之前调用 savefig():

1
2
3
4
5
6
fig.savefig(
"chart.png",
dpi=180,
bbox_inches="tight",
facecolor="white",
)
  • 网页图片通常用 PNG;
  • 需要无损缩放的线条图可以用 SVG 或 PDF;
  • dpi 主要影响栅格图的像素密度;
  • bbox_inches="tight" 可减少标题或图例被裁切的风险。

如果要在循环中批量生成大量图片,每轮保存后调用 plt.close(fig),避免未关闭的 Figure 持续占用内存。

十、一个可复用的绘图函数

将数据和样式放进函数时,让函数返回 fig, ax,调用方就能继续调整或决定保存位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import matplotlib.pyplot as plt


def plot_series(x, y, *, title, xlabel, ylabel):
fig, ax = plt.subplots(figsize=(8, 4), layout="constrained")
ax.plot(x, y, marker="o", linewidth=2)
ax.set(title=title, xlabel=xlabel, ylabel=ylabel)
ax.grid(alpha=0.25)
return fig, ax


fig, ax = plot_series(
[1, 2, 3, 4],
[3, 5, 4, 8],
title="指标变化",
xlabel="周期",
ylabel="值",
)
fig.savefig("metric.png", dpi=180, bbox_inches="tight")
plt.close(fig)

十一、发布前检查清单

  • 标题是否直接说明图表结论或主题;
  • 坐标轴是否有名称和单位;
  • 图例是否必要,名称是否准确;
  • 颜色是否有足够对比度,是否还用形状或线型辅助区分;
  • 柱状图是否从零开始,截断坐标轴是否明确说明;
  • 随机示例是否固定种子;
  • 导出的文字在手机或文档中是否仍然清晰;
  • 图表能否在不阅读正文的情况下被基本理解。

参考资料