Python 数据可视化入门:Matplotlib 核心用法

Python 数据可视化入门:Matplotlib 核心用法

摘要

数据表能够保存事实,但图表更适合展示趋势、差异、构成和异常。Matplotlib 是 Python 生态中基础而通用的绘图库,能够创建折线图、柱状图、散点图、直方图和多子图,并控制标题、坐标轴、图例、标注与输出文件。

本文从 Figure、Axes 和数据映射开始,介绍 Matplotlib 的基本绘图流程,重点演示业务数据中的趋势图、对比图、分布图和组合图。文章还会讨论图表类型选择、中文字体、坐标轴误导、图例遮挡、图片分辨率和可复现输出等实践问题。

一、背景与问题

1. 表格不一定能直接表达规律

假设有一份按月统计的销售数据:

text 复制代码
月份    线上销售额    门店销售额
一月       1200         900
二月       1350        1020
三月       1480        1100

直接阅读表格可以得到具体数字,但不容易快速观察:

  • 销售额是否持续增长;
  • 哪个渠道增长更快;
  • 两个渠道之间的差距是否扩大;
  • 某个月是否出现异常波动;
  • 不同指标的量级是否适合在同一张图中比较。

图表的价值不只是"把数据画出来",而是把数据中的关系编码成位置、长度、颜色、形状和连接线,让比较过程更直观。

2. 图表应服务于一个明确问题

常见的分析问题与图表类型可以这样对应:

分析问题 常见图表
随时间如何变化 折线图
不同类别谁更高 柱状图、条形图
两个数值变量是否相关 散点图
数值分布如何 直方图、箱线图
各类别占比如何 饼图或条形图
多个指标如何并列比较 多子图

如果一张图同时放入太多指标、颜色和注释,读者反而难以找到重点。

二、核心概念

1. Figure 和 Axes

Matplotlib 常用的对象层次如下:

text 复制代码
Figure
 ├── Axes
 │    ├── x 轴
 │    ├── y 轴
 │    ├── 标题
 │    └── 图例
 └── 其他 Axes
  • Figure 是整张画布;
  • Axes 是画布中的一个坐标系;
  • 一个 Figure 可以包含一个或多个 Axes。

推荐使用面向对象接口:

text 复制代码
fig, ax = plt.subplots()
ax.plot(x, y)

这样可以明确操作哪个坐标系,尤其适合多子图和复杂报表。

2. pyplot 和面向对象接口

matplotlib.pyplot 提供了简洁的状态式接口:

text 复制代码
plt.plot(x, y)
plt.title("趋势")
plt.show()

面向对象接口则把设置绑定到具体的 Axes:

text 复制代码
fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_title("趋势")
plt.show()

简单探索时两者都可以使用;正式脚本、报告和多图布局优先使用 fig, ax 方式。

3. 数据与视觉元素的映射

绘图时需要决定数据如何映射到视觉属性:

数据关系 视觉属性
时间先后 x 轴位置
指标数值 y 轴位置或柱子高度
分类字段 颜色、线型或分组位置
记录密度 点的数量或透明度
不确定性 误差线或阴影区域

映射关系越稳定,图表越容易解释。相同含义的渠道,应该尽量在不同图表中使用相同颜色。

4. 标题、坐标轴和图例

一张可读的图表至少需要:

  • 能说明业务含义的标题;
  • 清晰的 x 轴和 y 轴标签;
  • 单位或数量级;
  • 多系列数据对应的图例;
  • 合理的刻度和范围。

不要只写"销售趋势"这样的泛化标题,可以写成"2026 年第一季度各渠道销售额趋势(元)"。

三、工作原理

1. 创建画布、绘制图形、调整布局、输出文件

Matplotlib 的典型流程如下:

text 复制代码
准备数据
    ↓
创建 Figure 和 Axes
    ↓
调用 plot、bar、scatter 等绘图方法
    ↓
设置标题、坐标轴、图例和样式
    ↓
tight_layout 调整布局
    ↓
savefig 保存或 show 展示

绘图方法通常不会改变原始 DataFrame,因此数据清洗、统计和可视化可以保持相互分离。

2. 折线图连接的是有序观测

折线图适合时间序列或有明确顺序的数值。线段会把相邻点连接起来,因此必须先按 x 轴顺序排序:

text 复制代码
data = data.sort_values("month")
ax.plot(data["month"], data["revenue"])

如果类别没有自然顺序,直接使用折线连接可能制造不存在的趋势,此时更适合使用柱状图或散点图。

3. 柱状图通过长度比较数值

柱状图适合比较有限数量的类别。类别名称较长时,横向条形图通常比竖向柱状图更容易阅读:

text 复制代码
ax.bar(categories, values)
ax.barh(categories, values)

柱状图的基线通常从 0 开始,因为柱子长度代表数值大小。截断基线会放大差异,除非图表明确标注并且使用场景允许。

4. 多子图共享坐标轴

多个指标需要并排比较时,可以创建多子图:

text 复制代码
fig, axes = plt.subplots(2, 1, sharex=True)

共享 x 轴能够对齐时间位置;共享 y 轴则适合量纲相同的指标。不同单位的指标不应强行共享 y 轴。

5. 图像渲染与输出格式

show() 主要用于交互式查看,savefig() 用于保存文件。输出时要明确:

  • 文件格式:PNG 适合位图,SVG/PDF 适合矢量输出;
  • 分辨率:dpi 控制位图清晰度;
  • 画布尺寸:figsize 以英寸为单位;
  • 边距:bbox_inches="tight" 可以减少多余空白。

四、实战示例

下面的示例使用 Python 3.10+、pandas 2.1+ 和 Matplotlib 3.7+。运行前安装依赖:

text 复制代码
python -m pip install "pandas>=2.1" "matplotlib>=3.7"

1. 准备业务数据

python 复制代码
from pathlib import Path

import matplotlib.pyplot as plt
import pandas as pd


data = pd.DataFrame(
    {
        "month": ["一月", "二月", "三月", "四月", "五月", "六月"],
        "online": [1200, 1350, 1480, 1420, 1660, 1810],
        "store": [900, 1020, 1100, 1080, 1190, 1280],
        "orders": [120, 130, 145, 138, 156, 170],
    }
)

month_order = ["一月", "二月", "三月", "四月", "五月", "六月"]
data["month"] = pd.Categorical(
    data["month"],
    categories=month_order,
    ordered=True,
)
data = data.sort_values("month").reset_index(drop=True)

assert data["month"].tolist() == month_order
assert data[["online", "store", "orders"]].notna().all().all()

月份虽然是中文标签,但业务顺序不是字符串自然排序结果,因此显式设置有序分类。

2. 绘制销售额趋势折线图

python 复制代码
fig, ax = plt.subplots(figsize=(8, 4.5))

ax.plot(
    data["month"],
    data["online"],
    marker="o",
    linewidth=2,
    label="线上",
)
ax.plot(
    data["month"],
    data["store"],
    marker="o",
    linewidth=2,
    label="门店",
)

ax.set_title("2026 年上半年各渠道销售额趋势")
ax.set_xlabel("月份")
ax.set_ylabel("销售额(元)")
ax.grid(axis="y", alpha=0.3)
ax.legend(title="渠道")

fig.tight_layout()
fig.savefig("sales_trend.png", dpi=150, bbox_inches="tight")
plt.close(fig)

使用脚本批量生成图片时,保存后调用 plt.close(fig) 可以释放画布资源,避免循环绘图时内存持续增长。

3. 绘制渠道对比柱状图

python 复制代码
fig, ax = plt.subplots(figsize=(8, 4.5))

positions = range(len(data))
bar_width = 0.38

online_positions = [position - bar_width / 2 for position in positions]
store_positions = [position + bar_width / 2 for position in positions]

ax.bar(
    online_positions,
    data["online"],
    width=bar_width,
    label="线上",
)
ax.bar(
    store_positions,
    data["store"],
    width=bar_width,
    label="门店",
)

ax.set_xticks(list(positions))
ax.set_xticklabels(data["month"])
ax.set_title("各月份渠道销售额对比")
ax.set_xlabel("月份")
ax.set_ylabel("销售额(元)")
ax.legend()

fig.tight_layout()
fig.savefig("channel_comparison.png", dpi=150, bbox_inches="tight")
plt.close(fig)

分组柱状图中,两组柱子共享同一组月份位置,只是通过水平偏移并排显示。类别数量较多时,可以考虑改用横向条形图或分面图。

4. 绘制订单量与销售额散点图

python 复制代码
fig, ax = plt.subplots(figsize=(7, 4.5))

ax.scatter(
    data["orders"],
    data["online"] + data["store"],
    s=70,
    alpha=0.8,
)

for _, row in data.iterrows():
    ax.annotate(
        row["month"],
        (row["orders"], row["online"] + row["store"]),
        xytext=(5, 5),
        textcoords="offset points",
    )

ax.set_title("订单量与总销售额关系")
ax.set_xlabel("订单量")
ax.set_ylabel("总销售额(元)")
ax.grid(alpha=0.25)

fig.tight_layout()
fig.savefig("orders_revenue_scatter.png", dpi=150, bbox_inches="tight")
plt.close(fig)

散点图适合观察两个数值变量的关系。标注不宜过多,否则文字会互相遮挡;大数据集通常应减少标注或使用交互式工具。

5. 绘制销售额分布直方图

python 复制代码
revenue = (data["online"] + data["store"]).astype(float)

fig, ax = plt.subplots(figsize=(7, 4.5))
ax.hist(
    revenue,
    bins=4,
    edgecolor="white",
    alpha=0.85,
)
ax.axvline(
    revenue.mean(),
    color="red",
    linestyle="--",
    label=f"均值:{revenue.mean():.0f}",
)

ax.set_title("月度总销售额分布")
ax.set_xlabel("总销售额(元)")
ax.set_ylabel("月份数量")
ax.legend()

fig.tight_layout()
fig.savefig("revenue_distribution.png", dpi=150, bbox_inches="tight")
plt.close(fig)

直方图的箱数会影响分布外观。箱数太少会隐藏结构,太多则会产生噪声,应结合样本量和分析目的调整。

6. 创建多子图分析面板

python 复制代码
fig, axes = plt.subplots(
    2,
    2,
    figsize=(11, 7),
    constrained_layout=True,
)

axes[0, 0].plot(data["month"], data["online"], marker="o", label="线上")
axes[0, 0].plot(data["month"], data["store"], marker="o", label="门店")
axes[0, 0].set_title("销售额趋势")
axes[0, 0].legend()

axes[0, 1].bar(data["month"], revenue, color="#4C78A8")
axes[0, 1].set_title("月度总销售额")

axes[1, 0].scatter(data["orders"], revenue, color="#F58518")
axes[1, 0].set_title("订单量与销售额")
axes[1, 0].set_xlabel("订单量")
axes[1, 0].set_ylabel("销售额")

axes[1, 1].hist(revenue, bins=4, color="#54A24B")
axes[1, 1].set_title("销售额分布")

fig.suptitle("销售业务分析面板", fontsize=15)
fig.savefig("sales_dashboard.png", dpi=150, bbox_inches="tight")
plt.close(fig)

多子图适合把同一主题的多个视角放在一张报告图中。每个子图仍然需要清晰标题和必要的坐标轴说明。

7. 处理中文字体和负号

如果运行环境没有中文字体,标题和标签可能显示为方框。字体名称需要根据操作系统和服务器环境配置:

python 复制代码
import matplotlib.pyplot as plt


plt.rcParams["axes.unicode_minus"] = False
plt.rcParams["font.sans-serif"] = [
    "Microsoft YaHei",
    "SimHei",
    "DejaVu Sans",
]

生产环境不应假设某个字体一定存在。更稳妥的做法是把字体文件作为部署资产,使用 matplotlib.font_manager 注册并在生成前验证。

8. 封装统一的图片输出函数

python 复制代码
def save_figure(fig, output_dir, filename):
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)
    output_path = output_dir / filename
    fig.savefig(
        output_path,
        dpi=150,
        bbox_inches="tight",
        facecolor="white",
    )
    plt.close(fig)
    return output_path


fig, ax = plt.subplots(figsize=(6, 3.5))
ax.plot(data["month"], revenue, marker="o")
ax.set_title("月度总销售额")
ax.set_xlabel("月份")
ax.set_ylabel("销售额(元)")

output_path = save_figure(
    fig,
    "figures",
    "monthly_revenue.png",
)
assert output_path.exists()

统一封装输出参数可以减少不同图表的分辨率、边距和文件命名不一致问题。

五、常见问题与实践建议

1. 把类别顺序交给字符串排序

月份、季度、等级和业务流程状态都可能有自定义顺序。绘图前要通过有序分类、映射表或显式排序确定 x 轴顺序。

2. 用折线图连接无序类别

折线暗示相邻点存在连续变化。对于没有自然顺序的地区、产品类别或渠道,柱状图通常比折线图更合适。

3. 截断柱状图 y 轴

柱状图依靠柱子长度表达差异,通常应从 0 开始。若必须放大微小差异,可以使用点图、折线图或在标题和注释中明确 y 轴截断。

4. 图例和文字遮挡数据

图例、数据标签和注释都可能覆盖数据。可以调整图例位置、减少标注、增大画布,或使用 bbox_inches="tight" 配合人工检查输出图片。

5. 颜色使用过多

颜色越多不一定越清晰。分类数量较少时使用稳定的离散配色;连续数值应使用连续色带;不要只依赖颜色表达关键信息,还应配合位置、线型或文字。

6. 误差线没有定义

误差线必须说明表示标准差、标准误、置信区间还是业务上下限。没有定义的误差线会让图表看起来专业,却无法正确解释。

7. 在循环中不断创建图但不关闭

批量生成图表时,每次 plt.subplots() 都会创建对象。处理完后关闭 Figure,必要时使用非交互后端,避免服务器环境等待图形界面。

8. 只保存图片,不保存生成口径

报告图片还应保留数据范围、筛选条件、代码版本、绘图参数和生成时间。否则图片发生变化时,很难解释差异来源。

六、进阶思考

1. 图表应该表达比较关系

高质量图表通常围绕一个比较问题设计:

text 复制代码
谁更高?
增长多快?
差距是否扩大?
异常发生在哪里?
分布是否偏斜?

先写出问题,再选择图形和视觉编码,比先画一张"漂亮的图"更可靠。

2. 尺度会改变视觉结论

线性坐标适合观察绝对差异;对数坐标适合跨数量级比较增长倍数。使用对数轴时必须明确标注,否则读者可能把视觉距离误认为线性差值。

3. 不同单位不要直接共用 y 轴

销售额和订单量量纲不同。若需要同时展示,可以使用双 y 轴,但双轴容易造成虚假的相关性,优先考虑上下排列的多子图或先计算单位统一的指标。

4. 可视化函数应与数据准备分离

推荐分成三层:

text 复制代码
原始数据读取与清洗
        ↓
统计结果和图表数据准备
        ↓
绘图与文件输出

这样可以单独测试统计结果,也可以替换输出格式而不改变业务计算。

5. 可视化测试不只检查图片是否生成

自动化测试可以检查:

  • 输入字段是否存在;
  • 统计后的记录数和总量是否正确;
  • 输出文件是否存在且非空;
  • 图表标题、单位和文件名是否符合约定。

像素级图片比较容易受到字体和后端影响,通常应把数据断言与少量视觉抽查结合起来。

结论

Matplotlib 的核心工作流是创建 Figure 和 Axes,使用合适的绘图方法表达数据关系,再补充标题、坐标轴、图例、样式和输出配置。折线图适合趋势,柱状图适合类别比较,散点图适合关系分析,直方图适合分布观察,多子图适合组织同一主题的多个视角。

真正可靠的可视化还需要处理类别顺序、中文字体、坐标轴尺度、缺失值、输出分辨率和可复现性。下一步可以继续学习 Seaborn,在 Matplotlib 基础上快速绘制统计图表和分组分布。

参考资料

相关推荐
溪语流沙1 小时前
【Web全栈进阶】前端对接新契约:信封解包 + 统一错误处理
前端·vue.js·python·状态模式·fastapi
Python图像识别1 小时前
56-【2027毕设】YOLO11X光安检危险物品检测系统 - Python完整源码+PyQt5界面+训练模型+数据集
python·qt·课程设计
winfredzhang2 小时前
从零实现“照片按地理位置显示在矢量地图上”:Flask + SQLite + Leaflet 全栈实战
python·sqlite·flask
外收内放2 小时前
Python基础语法练习题(72)
开发语言·python
171320330计算机毕设编程2 小时前
计算机毕业设计 基于Hadoop的新能源汽车发展前景分析 LW PPT
java·大数据·hadoop·python·算法·随机森林·线性回归
科创致远2 小时前
科创致远 ESD 静电监控全景落地指南
人工智能·信息可视化·制造·精益工程
haluhalu.2 小时前
深入理解 AI Agent(1.2):Harness 工程——模型之外的竞争力
前端·数据结构·人工智能·python·langchain·个人开发
东莞市云毅网络有限公司3 小时前
把网页表格与列表还原成结构化条目:抽取与对账实现
python·文本处理·数据抽取
夜瞬3 小时前
从零微调 Qwen3-8B:基于 LoRA 与 LLaMA-Factory 打造专属 AI 编程助教
人工智能·python·llama