4. Matplotlib 快速上手:让数据说话
“一图胜千言”。在数据分析中,可视化是探索数据、发现规律、呈现结果的最直观、最有力的方式。本章,我们将学习 Python 数据可视化的基石——Matplotlib。
本章,你将学到:
-
Matplotlib 核心概念:理解
Figure(画布) 和Axes(坐标系) 的关系,以及两种绘图 API 的区别。 - 绘制常用图表: 掌握折线图、柱状图(含水平)、直方图、散点图、饼图和箱形图的绘制。
- 定制图表元素: 学会添加标题、标签、图例、网格,以及在图表上添加文本和旋转刻度。
-
多子图布局: 使用
subplots在一张画布上绘制多个图表。 - 保存图表: 将你的可视化结果保存为图片文件。
温馨提示
配套资源:本章所有代码和配套资源可以点击如下连接下载 —— 练习 Notebook。下载好后,同学可以在自己的 Anaconda 环境内运行这些代码,这有助于你们快速掌握相关内容。
Matplotlib: 可视化的瑞士军刀
Matplotlib 是 Python 生态中最著名、最基础的可视化库。虽然现在有许多更高级、更美观的库(如 Seaborn, Plotly),但它们中的许多都是基于 Matplotlib 构建的。因此,掌握 Matplotlib 的基本原理,能让你更好地理解和控制几乎所有的 Python 可视化工具。
Matplotlib 的设计理念是让你对图表的每一个元素都有完全的控制权,从坐标轴、刻度、标签到颜色、样式,无所不包。这使得它既能快速绘制简单的图表,也能实现高度定制化的复杂可视化。
有用链接:
准备工作
通常,我们导入 matplotlib.pyplot 模块,并按惯例将其重命名为 plt。同时,我们也需要 NumPy 和 Pandas 来准备数据。
在 Jupyter Notebook 中,通常会加上一行“魔法命令” %matplotlib inline,它能让图表直接嵌入在 Notebook 的输出单元格中。(在较新版本的 Jupyter 或 VS Code 中,这通常是自动的)。
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
np.random.seed(42)
plt.rcParams['axes.unicode_minus'] = False
# Jupyter Notebook/IPython 环境下的魔法命令
# %matplotlib inline
# 设置中文字体(此为示例,具体设置请参考第六章)
# plt.rcParams['font.sans-serif'] = ['SimHei']
一、Matplotlib 的核心:Figure 与 Axes
在开始画图前,我们需要理解两个核心概念:
-
Figure(画布): 这是最顶层的容器,你可以把它想象成一张空白的画布。一个Figure对象可以包含一个或多个Axes对象。 -
Axes(坐标系/子图): 这才是我们真正用来画图的地方,它包含了数据、坐标轴、标签、图例等图表的所有元素。一个Axes就是一个独立的图表(比如一个折线图或一个柱状图)。
一个比喻:Figure 就像一个画框,而 Axes 则是画框里的一幅幅画。你可以只有一个画框,里面放一幅大画(一个 Axes);也可以有一个大画框,里面用隔板分出好几个区域,每个区域放一幅小画(多个 Axes)。

最常用的创建 Figure 和 Axes 的方式是使用 plt.subplots()。
plt.subplots(nrows=1, ncols=1, figsize=None, sharex=False, sharey=False, constrained_layout=False)
| 参数 | 说明 |
|---|---|
nrows, ncols
|
(可选)子图的行数和列数。 |
figsize |
(可选)画布尺寸(宽, 高),单位为英寸。 |
sharex, sharey
|
(可选)是否共享 X/Y 轴,使多个子图刻度一致。 |
constrained_layout |
(可选)自动调整子图布局以避免重叠。 |
# 创建一个 Figure 和一个 Axes
# fig 是整个画布对象,ax 是画布中的一个子图(坐标系)对象
fig, ax = plt.subplots(figsize=(6, 4))
# 现在,我们就可以在 ax 上进行绘图了
# (这里我们先不画任何东西,只是展示创建过程)
# 最后,使用 plt.show() 来显示这张画布
plt.show()
二、一个重要的提醒:两种绘图接口(API)
Matplotlib 有两种使用风格,初学者经常会感到困惑:
-
面向对象的接口 (Object-Oriented API): 这是我们强烈推荐并将在本教程中始终使用的方法。你显式地创建
Figure和Axes对象,然后调用这些对象的方法来绘图 (e.g.,ax.plot(),ax.set_title())。它的代码结构清晰,能更好地控制复杂的图表。 -
基于状态的接口 (State-based API): 这种方式依赖于
pyplot模块 (plt) 来“记住”当前正在操作的图表。你直接调用plt.plot(),plt.title()等函数。它的优点是写起来快,适合快速、简单的绘图。
# 准备数据
x = np.linspace(0, 10, 100)
y = np.sin(x)
# 1. 面向对象风格 (推荐)
fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_title("面向对象 (OO) 风格")
plt.show()
# 2. Pyplot 风格 (不推荐用于复杂图表)
plt.figure() # 创建一个全局图表
plt.plot(x, y)
plt.title("Pyplot 风格")
plt.show()
当你看到网上的示例直接使用 plt.plot() 时,就是在用第二种风格。虽然它也能工作,但我们强烈建议你从一开始就养成使用面向对象接口的习惯,因为它更强大、更不容易出错。
三、绘制常见的图表
现在,让我们在坐标系 (ax) 上绘制一些最常见的图表。
1. 折线图 (plot)
用途:折线图最适合用来展示数据在一个连续区间(通常是时间)上的变化趋势。
Axes.plot(x, y, *, color=None, linestyle=None, linewidth=None, marker=None, label=None, alpha=None)
| 参数 | 说明 |
|---|---|
x, y
|
(必选)要绘制的数据序列。 |
color |
(可选)线条颜色,可以是颜色字符串或十六进制色值。 |
linestyle |
(可选)线型,如 '-', '--', ':'。 |
linewidth |
(可选)线宽,默认 1.5。 |
marker |
(可选)数据点标记样式,如 'o', 's'。 |
label |
(可选)图例标签。 |
alpha |
(可选)透明度(0~1)。 |
# 准备数据:x 轴是时间点,y 轴是对应的数值
x_data = np.linspace(0, 10, 100)
y_data = np.sin(x_data)
# 创建 Figure 和 Axes
fig, ax = plt.subplots(figsize=(8, 4))
# 在 ax 上绘制折线图
ax.plot(x_data, y_data, color='steelblue', linewidth=2, marker='o', markevery=10, label='sin(x)')
ax.set_title("简单的正弦函数折线图")
ax.set_xlabel("X 值")
ax.set_ylabel("sin(X)")
ax.legend(loc='best')
# 显示图表
plt.show()

2. 柱状图 (bar & barh)
用途:柱状图用于比较不同类别下的数值大小。当类别名称较长时,使用水平柱状图 (barh) 会更清晰。
Axes.bar(x, height, width=0.8, color=None, label=None, alpha=None)
| 参数 | 说明 |
|---|---|
x |
(必选)柱子的类别或位置。 |
height |
(必选)柱子的数值高度。 |
width |
(可选)柱体宽度。 |
color |
(可选)柱子的填充颜色,可以是单个或列表。 |
label |
(可选)图例标签。 |
alpha |
(可选)透明度。 |
Axes.barh(y, width, height=0.8, color=None, label=None, alpha=None)
| 参数 | 说明 |
|---|---|
y |
(必选)柱子的类别或位置。 |
width |
(必选)水平柱的长度。 |
height |
(可选)柱体厚度。 |
color, label, alpha
|
(可选)与 bar 中含义一致。 |
# 准备数据:类别和对应的数值
categories = ['第一季度', '第二季度', '第三季度', '第四季度']
values = [150, 230, 180, 210]
# 创建一个包含两个子图的 Figure
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4), constrained_layout=True)
# 在左侧子图上绘制垂直柱状图
ax1.bar(categories, values, width=0.6, color=['#ff9999', '#66b3ff', '#99ff99', '#ffcc99'], edgecolor='black', label='季度销售')
ax1.set_title('垂直柱状图 (bar)')
ax1.set_ylabel('销售额 (万元)')
ax1.legend()
# 在右侧子图上绘制水平柱状图
ax2.barh(categories, values, height=0.6, color=['#ff9999', '#66b3ff', '#99ff99', '#ffcc99'], label='季度销售', alpha=0.8)
ax2.set_title('水平柱状图 (barh)')
ax2.set_xlabel('销售额 (万元)')
ax2.legend(loc='lower right')
fig.suptitle('年度销售额对比')
plt.show()

3. 直方图 (hist)
用途:直方图用于展示单个数值型变量的分布情况。它会将数据划分成若干个“箱子”(bins),然后统计每个箱子里的数据点数量。
Axes.hist(x, bins=None, range=None, density=False, color=None, alpha=None, edgecolor=None)
| 参数 | 说明 |
|---|---|
x |
(必选)输入数据(数组或序列)。 |
bins |
(可选)分箱数或自定义分箱边界。 |
range |
(可选)指定分箱范围 (min, max)。 |
density |
(可选)为 True 时显示概率密度而非频数。 |
color |
(可选)柱子的填充颜色。 |
alpha |
(可选)透明度。 |
edgecolor |
(可选)柱子边框颜色。 |
# 准备数据:生成 1000 个服从标准正态分布的随机数
data = np.random.randn(1000)
# 创建 Figure 和 Axes
fig, ax = plt.subplots(figsize=(7, 4))
# 在 ax 上绘制直方图,bins=30 表示将数据分成 30 个箱子
ax.hist(data, bins=30, range=(-4, 4), color='#66b3ff', alpha=0.8, edgecolor='black')
ax.set_title("随机数据的分布直方图")
ax.set_xlabel("数值")
ax.set_ylabel("频数")
# 显示图表
plt.show()

4. 散点图 (scatter)
用途:散点图用于观察两个数值型变量之间的关系(例如,正相关、负相关或不相关)。
Axes.scatter(x, y, s=None, c=None, cmap=None, alpha=None, edgecolors=None, label=None)
| 参数 | 说明 |
|---|---|
x, y
|
(必选)点的坐标序列。 |
s |
(可选)点的大小(标量或数组)。 |
c |
(可选)点的颜色,可为数组表示颜色映射。 |
cmap |
(可选)当 c 为数值数组时使用的颜色图。 |
alpha |
(可选)透明度。 |
edgecolors |
(可选)点的边框颜色。 |
label |
(可选)图例标签。 |
# 准备数据:两个相关的变量
x_scatter = np.random.rand(50) * 10
# y 与 x 呈线性关系,并加入一些随机噪声
y_scatter = 2 * x_scatter + 1 + np.random.randn(50) * 2
# 创建 Figure 和 Axes
fig, ax = plt.subplots(figsize=(6, 4))
# 在 ax 上绘制散点图
sizes = (np.random.rand(50) * 80) + 20
ax.scatter(x_scatter, y_scatter, s=sizes, c=x_scatter, cmap='viridis', alpha=0.7, edgecolors='black', label='样本点')
ax.set_title("X 和 Y 的散点关系图")
ax.set_xlabel("变量 X")
ax.set_ylabel("变量 Y")
ax.grid(True)
ax.legend(loc='upper left')
# 显示图表
plt.show()

5. 饼图 (pie)
用途:饼图用于展示各个部分占整体的构成比例。
注意:虽然饼图在商业报告中很常见,但在数据分析领域请谨慎使用。当类别过多或比例相近时,人眼很难准确比较各个扇区的大小。柱状图通常是更好的替代品。
Axes.pie(x, explode=None, labels=None, autopct=None, startangle=0, shadow=False, colors=None, pctdistance=0.6)
| 参数 | 说明 |
|---|---|
x |
(必选)各部分的数值序列。 |
explode |
(可选)用于突出显示的偏移量序列。 |
labels |
(可选)每个扇区的标签。 |
autopct |
(可选)控制百分比显示的格式字符串。 |
startangle |
(可选)起始角度。 |
shadow |
(可选)是否绘制阴影。 |
colors |
(可选)扇区颜色列表。 |
pctdistance |
(可选)百分数字体距离圆心的比例。 |
# 准备数据
labels = ['市场部', '研发部', '销售部', '行政部']
sizes = [15, 30, 45, 10] # 各部门人数
explode = (0, 0, 0.1, 0) # '销售部' 稍微突出显示
fig, ax = plt.subplots()
# 绘制饼图
# autopct='%1.1f%%' 会在扇区上显示百分比
colors = ['#ff9999', '#66b3ff', '#ffcc99', '#99ff99']
ax.pie(
sizes,
explode=explode,
labels=labels,
colors=colors,
autopct='%1.1f%%',
shadow=True,
startangle=90,
pctdistance=0.8
)
# 保证饼图是正圆的
ax.axis('equal')
ax.set_title('公司各部门人数占比')
plt.show()

6. 箱形图 (boxplot)
用途:箱形图(或称箱线图)是展示一组或多组数据分布情况和异常值的利器。它能清晰地显示出数据的中位数、四分位数、范围和任何潜在的异常点。
Axes.boxplot(x, notch=None, vert=True, patch_artist=False, labels=None, showfliers=True, widths=None)
| 参数 | 说明 |
|---|---|
x |
(必选)要绘制的数据序列或序列列表。 |
notch |
(可选)为 True 时绘制凹口箱体,以估计中位数置信区间。 |
vert |
(可选)控制箱线垂直 (True) 或水平 (False)。 |
patch_artist |
(可选)允许使用颜色填充箱体。 |
tick_labels |
(可选)每个箱子的标签。 |
showfliers |
(可选)是否显示异常值(离群点)。 |
widths |
(可选)控制每个箱子的宽度。 |
# 准备数据:三组不同分布的数据
np.random.seed(10)
data1 = np.random.normal(100, 10, 200)
data2 = np.random.normal(80, 30, 200)
data3 = np.random.normal(90, 20, 200)
data_to_plot = [data1, data2, data3]
fig, ax = plt.subplots()
# 绘制箱形图
bp = ax.boxplot(
data_to_plot,
patch_artist=True,
labels=['A产品', 'B产品', 'C产品'],
showfliers=True,
widths=0.6
)
# 为箱子填充颜色
colors = ['#99ff99', '#66b3ff', '#ff9999']
for patch, color in zip(bp['boxes'], colors):
patch.set_facecolor(color)
ax.set_title('不同产品销售额分布对比')
ax.set_ylabel('销售额')
ax.yaxis.grid(True)
plt.show()

四、定制你的图表:让信息更清晰
一个“裸”图表很难传递有效信息。我们需要为它添加标题、标签、图例等元素,让它变得清晰易读。
1. 添加标题、标签和图例
| 方法 | 常用参数 | 说明 |
|---|---|---|
Axes.set_title(label, loc='center', pad=None) |
label(必选); loc(可选): 标题位置 ('left', 'center', 'right'); pad(可选): 标题与图表的距离 |
设置子图标题。 |
Axes.set_xlabel(label, fontsize=None) |
label(必选); fontsize(可选): 字体大小 |
设置 X 轴标签。 |
Axes.set_ylabel(label, fontsize=None) |
label(必选); fontsize(可选): 字体大小 |
设置 Y 轴标签。 |
Axes.legend(loc='best', ncol=1, frameon=True) |
loc(可选): 图例位置;ncol(可选): 列数;frameon(可选): 是否显示边框 |
添加图例。 |
# 准备数据
x = np.linspace(0, 2 * np.pi, 100)
y1 = np.sin(x)
y2 = np.cos(x)
# 创建 Figure 和 Axes
fig, ax = plt.subplots()
# 绘制两条折线,并为它们设置 label,用于后续生成图例
ax.plot(x, y1, label='sin(x)')
ax.plot(x, y2, label='cos(x)', linestyle='--')
# 1. 添加标题
ax.set_title('正弦与余弦函数图像', loc='left', pad=12)
# 2. 添加 X 轴和 Y 轴的标签
ax.set_xlabel('X 轴 (弧度)', fontsize=11)
ax.set_ylabel('Y 轴 (值)', fontsize=11)
# 3. 添加图例 (它会自动寻找 plot 中设置的 label)
ax.legend(loc='best', ncol=2)
plt.show()

2. 调整样式、颜色和网格
| 方法 | 常用参数 | 说明 |
|---|---|---|
Axes.grid(visible=True, which='major', axis='both', linestyle='--', alpha=None) |
visible(可选)控制网格可见性;which(可选)网格类型;axis(可选)作用轴;linestyle(可选)样式;alpha(可选)透明度。 |
|
Axes.set_xlim(left=None, right=None) |
left(可选)和 right(可选)设置 X 轴范围。 |
|
Axes.set_ylim(bottom=None, top=None) |
bottom(可选)和 top(可选)设置 Y 轴范围。 |
|
Axes.plot(..., color=None, linestyle=None, marker=None, linewidth=None) |
color(可选)、linestyle(可选)、marker(可选)、linewidth(可选)自定义线条样式与标记。 |
fig, ax = plt.subplots()
# 绘制时指定颜色、线型、标记样式
ax.plot(x, y1, label='sin(x)', color='blue', linestyle='-', marker='o', markersize=3, linewidth=2)
ax.plot(x, y2, label='cos(x)', color='red', linestyle=':', linewidth=2)
# 添加网格
ax.grid(visible=True, linestyle=':', alpha=0.6)
# 设置坐标轴范围
ax.set_xlim(0, 2 * np.pi)
ax.set_ylim(-1.5, 1.5)
ax.set_title('定制化样式的图表')
ax.legend()
plt.show()

3. 添加文本注解和旋转刻度
有时我们需要在图表的特定位置高亮数据,或者当坐标轴标签太长而重叠时,需要对它们进行调整。
| 方法 | 常用参数 | 说明 |
|---|---|---|
Axes.tick_params(axis='x', rotation=None, labelsize=None) |
axis(必选)指定作用轴;rotation(可选)刻度方向;labelsize(可选)标签大小。 |
|
Axes.text(x, y, s, ha='center', va='center', fontsize=None) |
x(必选)、y(必选)位置;s(必选)文本内容;ha(可选)水平对齐;va(可选)垂直对齐;fontsize(可选)字号。 |
|
Figure.tight_layout(pad=1.08) |
pad(可选)调整整个画布的布局,避免元素重叠。 |
# 准备数据
categories = ['非常不满意', '不满意', '一般', '满意', '非常满意']
values = [5, 25, 50, 120, 200]
fig, ax = plt.subplots(figsize=(8, 5))
bars = ax.bar(categories, values, color='#66b3ff')
# 1. 旋转 X 轴的刻度标签,以防重叠
ax.tick_params(axis='x', rotation=45)
# 2. 在每个柱子顶端添加数值标签
for bar in bars:
yval = bar.get_height()
ax.text(
bar.get_x() + bar.get_width() / 2.0,
yval,
f"{int(yval)}",
va='bottom',
ha='center',
fontsize=10,
fontweight='bold'
)
ax.set_title('客户满意度调查结果')
ax.set_ylabel('投票数')
fig.tight_layout() # 调整布局以防标签被截断
plt.show()

五、多子图布局 (subplots)
当你想将多个相关的图表并排比较时,就需要用到多子图布局。plt.subplots() 函数是实现这一点的最佳方式。
| 参数 | 说明 |
|---|---|
nrows, ncols
|
(可选)子图网格的行列数。 |
figsize |
(可选)整个画布大小。 |
sharex, sharey
|
(可选)在多个子图之间共享坐标轴。 |
squeeze |
(可选)控制返回的 axes 维度,False 时总是返回二维数组。 |
gridspec_kw |
(可选)传递给底层 GridSpec 的字典,可细致控制布局。 |
# 创建一个 2x2 的子图网格
# fig 是整个画布,axes 是一个 2x2 的 NumPy 数组,每个元素都是一个子图对象
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10, 8), constrained_layout=True)
# --- 在每个子图上绘图 ---
# 左上角子图 (axes[0, 0])
axes[0, 0].plot(np.sin(np.linspace(0, 10, 100)))
axes[0, 0].set_title('折线图')
# 右上角子图 (axes[0, 1])
axes[0, 1].bar(['A', 'B', 'C'], [3, 5, 2])
axes[0, 1].set_title('柱状图')
# 左下角子图 (axes[1, 0])
axes[1, 0].hist(np.random.randn(500), bins=20)
axes[1, 0].set_title('直方图')
# 右下角子图 (axes[1, 1])
axes[1, 1].scatter(np.random.rand(50), np.random.rand(50))
axes[1, 1].set_title('散点图')
# 显示整个画布
plt.show()

六、保存图表
将图表保存到文件中非常简单,只需使用 fig.savefig() 方法。
| 方法 | 常用参数 | 说明 |
|---|---|---|
Figure.savefig(fname, dpi=None, bbox_inches=None, facecolor=None, transparent=False) |
fname(必选): 输出路径;dpi(可选): 分辨率;bbox_inches(可选): 调整图像边界;facecolor(可选): 背景颜色;transparent(可选): 是否透明背景。 |
from pathlib import Path
output_path = Path("assets/img_ana/lesson2/matplotlib")
output_path.mkdir(parents=True, exist_ok=True)
fig, ax = plt.subplots()
x = np.linspace(0, 2 * np.pi, 100)
y1 = np.sin(x)
ax.plot(x, y1, label='sin(x)')
ax.set_title('保存这张图')
ax.legend()
save_path = output_path / "figure_4_12_saved_plot.png"
fig.savefig(save_path, dpi=300, bbox_inches='tight', facecolor='white', transparent=False)
print(f"图表已保存为 {save_path}")

关于中文显示问题 Matplotlib 默认的字体不支持中文,因此如果你在图表中直接使用中文,可能会显示为方框
□。这是一个常见问题,我们将在 第六章:常见问题与排查 中提供详细的解决方案。
实践小结: 你已经学会了使用 Matplotlib 绘制和定制最常见的几种基本图表,并且掌握了如何将它们组合在同一张画布上以及如何将结果保存。可视化是数据故事叙述的开始。下一章,我们将把前面三章所学的 NumPy, Pandas, Matplotlib 知识串联起来,完成一个从数据加载、处理到可视化的端到端小案例。