8 minutes
数据可视化入门
数据可视化是数据分析中最重要的环节之一。一张好的图表能抵过千言万语——它能让复杂的数据模式一目了然,让隐藏在数字背后的故事变得清晰可见。本篇文章将介绍 Matplotlib 的核心用法,以及如何用 Python 创建专业的数据可视化图表。
Matplotlib 核心概念
Matplotlib 是 Python 中最经典的数据可视化库。虽然它的默认样式偏保守,但通过自定义可以制作出高质量的图表。
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
# Matplotlib 的核心是分层结构:
# - Figure: 画布(整个图表窗口)
# - Axes: 坐标系(实际绘制图表的地方)
# - Axis: 坐标轴(x 轴和 y 轴)
# - Artist: 所有可见元素(线条、文字、标记等)
# 最基本的绘图模式
plt.figure(figsize=(8, 5)) # 创建画布
plt.plot([1, 2, 3, 4], [1, 4, 9, 16]) # 绘制折线
plt.xlabel("x 轴")
plt.ylabel("y 轴")
plt.title("最简单的折线图")
plt.show()
Figure 和 Axes(面向对象方式)
推荐使用面向对象的方式绘图,它更加灵活可控:
# 创建 Figure 和 Axes
fig, ax = plt.subplots(figsize=(8, 5))
# 在这个 Axes 上绘制
ax.plot([1, 2, 3, 4], [1, 4, 9, 16], label="数据系列")
ax.set_xlabel("x 轴标签", fontsize=12)
ax.set_ylabel("y 轴标签", fontsize=12)
ax.set_title("图表标题", fontsize=14)
ax.legend()
ax.grid(True, alpha=0.3)
plt.show()
常见图表类型
折线图 (Line Plot)
折线图适合展示数据随时间的变化趋势:
# 生成数据
x = np.linspace(0, 10, 100)
y1 = np.sin(x)
y2 = np.cos(x)
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(x, y1, label="sin(x)", color="#2196F3", linewidth=2, linestyle="-")
ax.plot(x, y2, label="cos(x)", color="#F44336", linewidth=2, linestyle="--")
ax.set_xlabel("x", fontsize=12)
ax.set_ylabel("y", fontsize=12)
ax.set_title("正弦和余弦函数", fontsize=14)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
plt.show()
# 折线图的常用参数
# color: 线条颜色
# linewidth/lw: 线条粗细
# linestyle/ls: 线型(-, --, -., :)
# marker: 数据点标记(o, s, ^, D, *)
# markersize: 标记大小
# alpha: 透明度(0-1)
散点图 (Scatter Plot)
散点图用于展示两个变量之间的关系:
# 生成数据
np.random.seed(42)
n = 200
x = np.random.randn(n)
y = 2 * x + np.random.randn(n) * 0.5
colors = np.random.rand(n)
sizes = np.random.randint(20, 200, n)
fig, ax = plt.subplots(figsize=(8, 6))
scatter = ax.scatter(
x, y,
c=colors, # 根据数值着色
s=sizes, # 点的大小
alpha=0.6,
cmap="viridis", # 颜色映射
edgecolors="white", # 描边颜色
linewidth=0.5
)
# 添加颜色条
plt.colorbar(scatter, ax=ax, label="颜色值")
ax.set_xlabel("X 变量", fontsize=12)
ax.set_ylabel("Y 变量", fontsize=12)
ax.set_title("散点图示例", fontsize=14)
ax.grid(True, alpha=0.3)
plt.show()
# 气泡图:用点的大小表示第三个维度
# 上面的 sizes 变量已经实现了这一点
柱状图 (Bar Chart)
柱状图适合展示分类数据的对比:
# 基本柱状图
categories = ["产品 A", "产品 B", "产品 C", "产品 D", "产品 E"]
values = [45, 62, 38, 71, 53]
fig, ax = plt.subplots(figsize=(8, 5))
bars = ax.bar(
categories, values,
color=["#2196F3", "#4CAF50", "#FF9800", "#F44336", "#9C27B0"],
edgecolor="white",
linewidth=1.5,
alpha=0.85
)
# 在柱子上显示数值
for bar, value in zip(bars, values):
ax.text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + 1,
str(value),
ha="center", va="bottom",
fontsize=11
)
ax.set_xlabel("产品类别", fontsize=12)
ax.set_ylabel("销售额(万元)", fontsize=12)
ax.set_title("各产品销售对比", fontsize=14)
ax.set_ylim(0, 85)
ax.grid(axis="y", alpha=0.3)
plt.show()
# 水平柱状图
fig, ax = plt.subplots(figsize=(8, 5))
ax.barh(categories, values, color="#4CAF50", alpha=0.8)
ax.set_xlabel("销售额(万元)")
ax.set_title("各产品销售对比(水平)")
plt.show()
# 分组柱状图
categories = ["Q1", "Q2", "Q3", "Q4"]
product_a = [25, 32, 28, 35]
product_b = [18, 25, 30, 28]
product_c = [22, 20, 26, 24]
x = np.arange(len(categories))
width = 0.25
fig, ax = plt.subplots(figsize=(10, 6))
ax.bar(x - width, product_a, width, label="产品 A", color="#2196F3")
ax.bar(x, product_b, width, label="产品 B", color="#4CAF50")
ax.bar(x + width, product_c, width, label="产品 C", color="#FF9800")
ax.set_xlabel("季度", fontsize=12)
ax.set_ylabel("销售额(万元)", fontsize=12)
ax.set_title("各产品季度销售额对比", fontsize=14)
ax.set_xticks(x)
ax.set_xticklabels(categories)
ax.legend()
ax.grid(axis="y", alpha=0.3)
plt.show()
# 堆叠柱状图
fig, ax = plt.subplots(figsize=(10, 6))
ax.bar(categories, product_a, label="产品 A", color="#2196F3")
ax.bar(categories, product_b, bottom=product_a, label="产品 B", color="#4CAF50")
ax.bar(categories, product_c, bottom=np.array(product_a) + np.array(product_b),
label="产品 C", color="#FF9800")
ax.set_xlabel("季度", fontsize=12)
ax.set_ylabel("销售额(万元)", fontsize=12)
ax.set_title("各季度总销售额构成", fontsize=14)
ax.legend()
ax.grid(axis="y", alpha=0.3)
plt.show()
直方图 (Histogram)
直方图展示数据的分布情况:
# 生成数据
np.random.seed(42)
data_normal = np.random.normal(70, 15, 1000) # 正态分布
data_skewed = np.random.exponential(50, 1000) # 偏态分布
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# 正态分布
axes[0].hist(
data_normal,
bins=30, # 组数
color="#2196F3",
edgecolor="white",
alpha=0.7,
density=True # 归一化为概率密度
)
axes[0].set_title("正态分布(考试成绩)", fontsize=13)
axes[0].set_xlabel("分数")
axes[0].set_ylabel("密度")
axes[0].grid(alpha=0.3)
# 偏态分布
axes[1].hist(
data_skewed,
bins=30,
color="#FF9800",
edgecolor="white",
alpha=0.7,
density=True
)
axes[1].set_title("偏态分布(等待时间)", fontsize=13)
axes[1].set_xlabel("分钟")
axes[1].set_ylabel("密度")
axes[1].grid(alpha=0.3)
plt.tight_layout()
plt.show()
# 多组直方图对比
data_a = np.random.normal(65, 10, 500)
data_b = np.random.normal(75, 12, 500)
fig, ax = plt.subplots(figsize=(10, 5))
ax.hist(data_a, bins=20, alpha=0.6, label="A 班", color="#2196F3")
ax.hist(data_b, bins=20, alpha=0.6, label="B 班", color="#F44336")
ax.set_xlabel("分数", fontsize=12)
ax.set_ylabel("人数", fontsize=12)
ax.set_title("A 班和 B 班成绩分布对比", fontsize=14)
ax.legend(fontsize=12)
ax.grid(alpha=0.3)
plt.show()
饼图 (Pie Chart)
饼图展示各部分的占比:
# 基本饼图
labels = ["北京", "上海", "广州", "深圳", "杭州"]
sizes = [35, 28, 18, 12, 7]
explode = (0.05, 0.05, 0.05, 0.05, 0.05) # 分离部分
colors = ["#2196F3", "#4CAF50", "#FF9800", "#F44336", "#9C27B0"]
fig, ax = plt.subplots(figsize=(8, 8))
wedges, texts, autotexts = ax.pie(
sizes,
labels=labels,
autopct="%1.1f%%", # 显示百分比
startangle=90, # 起始角度
explode=explode,
colors=colors,
shadow=True,
textprops={"fontsize": 12}
)
# 设置百分比文字样式
for autotext in autotexts:
autotext.set_color("white")
autotext.set_fontsize(11)
autotext.set_fontweight("bold")
ax.set_title("各区域销售额占比", fontsize=14, pad=20)
plt.show()
# 环形图(饼图 + 中心空白)
fig, ax = plt.subplots(figsize=(8, 8))
ax.pie(
sizes,
labels=labels,
autopct="%1.1f%%",
startangle=90,
colors=colors,
wedgeprops={"width": 0.4} # 设置环形宽度
)
ax.set_title("各区域销售额占比(环形图)", fontsize=14, pad=20)
plt.show()
图表自定义
颜色、样式和主题
# 颜色名称和代码
# Matplotlib 支持多种颜色指定方式:
# - 名称: "red", "blue", "green"
# - 十六进制: "#FF5733"
# - RGB 元组: (0.2, 0.4, 0.6)
# - 灰度: "0.5"(0 黑,1 白)
# 预设颜色映射(cmap)
# 连续数据: "viridis", "plasma", "inferno", "magma"
# 顺序数据: "Blues", "Reds", "Greens"
# 发散数据: "RdBu", "coolwarm", "PiYG"
# 使用 Seaborn 样式(更现代的默认样式)
import seaborn as sns
sns.set_theme() # 应用 seaborn 默认样式
# sns.set_style("whitegrid")
# sns.set_palette("husl")
# Matplotlib 内置样式
print(plt.style.available)
# ['seaborn-v0_8', 'ggplot', 'fivethirtyeight', 'dark_background', ...]
plt.style.use("ggplot") # 应用 R 语言 ggplot2 风格
完整自定义示例
# 生成数据
x = np.linspace(0, 2 * np.pi, 50)
y1 = np.sin(x)
y2 = np.sin(2 * x)
y3 = np.sin(3 * x)
fig, ax = plt.subplots(figsize=(12, 6))
# 绘制三条曲线
ax.plot(x, y1, label="sin(x)", color="#E74C3C", lw=2, marker="o", markersize=4)
ax.plot(x, y2, label="sin(2x)", color="#3498DB", lw=2, marker="s", markersize=4)
ax.plot(x, y3, label="sin(3x)", color="#2ECC71", lw=2, marker="^", markersize=4)
# 标题和轴标签
ax.set_title("正弦函数家族", fontsize=16, fontweight="bold", pad=15)
ax.set_xlabel("x (弧度)", fontsize=12)
ax.set_ylabel("y", fontsize=12)
# 图例
ax.legend(loc="upper right", fontsize=11, framealpha=0.9,
edgecolor="#CCCCCC")
# 网格
ax.grid(True, alpha=0.3, linestyle="--", color="#999999")
# 坐标轴范围
ax.set_xlim(0, 2 * np.pi)
ax.set_ylim(-1.5, 1.5)
# 刻度标签
ax.set_xticks([0, np.pi/2, np.pi, 3*np.pi/2, 2*np.pi])
ax.set_xticklabels(["0", "π/2", "π", "3π/2", "2π"], fontsize=10)
ax.tick_params(axis="both", labelsize=10)
# 添加水平参考线
ax.axhline(y=0, color="black", lw=0.8, alpha=0.5)
ax.axvline(x=0, color="black", lw=0.8, alpha=0.5)
# 添加文本标注
ax.annotate(
"峰值", xy=(np.pi/2, 1), xytext=(np.pi/2 + 0.5, 1.3),
arrowprops=dict(facecolor="black", shrink=0.05, width=1.5),
fontsize=11
)
plt.tight_layout()
plt.show()
子图 (Subplots)
在同一个画布上展示多个图表:
# 创建 2x2 子图
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# 左上:折线图
x = np.linspace(0, 10, 100)
axes[0, 0].plot(x, np.sin(x), color="#E74C3C")
axes[0, 0].set_title("折线图")
axes[0, 0].grid(alpha=0.3)
# 右上:散点图
np.random.seed(42)
x_rand = np.random.randn(100)
y_rand = x_rand + np.random.randn(100) * 0.5
axes[0, 1].scatter(x_rand, y_rand, alpha=0.6, color="#3498DB")
axes[0, 1].set_title("散点图")
axes[0, 1].grid(alpha=0.3)
# 左下:柱状图
categories = ["A", "B", "C", "D", "E"]
values = np.random.randint(10, 50, 5)
axes[1, 0].bar(categories, values, color="#2ECC71", alpha=0.8)
axes[1, 0].set_title("柱状图")
axes[1, 0].grid(axis="y", alpha=0.3)
# 右下:直方图
data = np.random.randn(500)
axes[1, 1].hist(data, bins=20, color="#9B59B6", alpha=0.7, edgecolor="white")
axes[1, 1].set_title("直方图")
axes[1, 1].grid(alpha=0.3)
plt.tight_layout() # 自动调整子图间距
plt.show()
# 不同大小的子图(使用 gridspec)
from matplotlib.gridspec import GridSpec
fig = plt.figure(figsize=(12, 8))
gs = GridSpec(3, 3, figure=fig)
ax1 = fig.add_subplot(gs[0, :]) # 第一行,占满所有列
ax2 = fig.add_subplot(gs[1, :-1]) # 第二行,占前两列
ax3 = fig.add_subplot(gs[1:, -1]) # 第二行和第三行,最后一列
ax4 = fig.add_subplot(gs[2, 0]) # 第三行,第一列
ax5 = fig.add_subplot(gs[2, 1]) # 第三行,第二列
ax1.plot(x, np.sin(x), color="#E74C3C")
ax2.scatter(x_rand, y_rand, alpha=0.6, color="#3498DB")
ax3.barh(categories, values, color="#2ECC71")
ax4.hist(data, bins=20, color="#9B59B6")
ax5.plot(x, np.cos(x), color="#F39C12")
ax1.set_title("多种布局子图", fontsize=14)
plt.tight_layout()
plt.show()
保存图表
# 保存为 PNG 图片
fig.savefig("chart.png", dpi=150, bbox_inches="tight")
# 保存为 PDF(矢量格式)
fig.savefig("chart.pdf", bbox_inches="tight")
# 保存为 SVG(矢量格式,适合网页使用)
fig.savefig("chart.svg", bbox_inches="tight")
# 保存时去白边
fig.savefig("chart.png", dpi=150, bbox_inches="tight", pad_inches=0.1)
# 透明背景
fig.savefig("chart_transparent.png", dpi=150, transparent=True)
与 Pandas 集成绘图
Pandas 的 DataFrame 和 Series 对象内置了绘图方法:
# 创建示例数据
dates = pd.date_range("2024-01-01", periods=100, freq="D")
df = pd.DataFrame({
"日期": dates,
"销售额": np.random.randint(1000, 5000, 100),
"成本": np.random.randint(500, 3000, 100)
})
df.set_index("日期", inplace=True)
# 直接使用 Pandas 绘图
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 折线图
df.plot(ax=axes[0, 0], title="销售额与成本趋势", grid=True)
# 折线图(仅销售额)
df["销售额"].plot(ax=axes[0, 1], title="日销售额趋势",
color="#3498DB", style="o-", grid=True)
# 直方图
df["销售额"].plot(ax=axes[1, 0], kind="hist", bins=20,
title="销售额分布", edgecolor="white", color="#2ECC71")
# 箱线图
df[["销售额", "成本"]].plot(ax=axes[1, 1], kind="box", title="数据分布箱线图")
plt.tight_layout()
plt.show()
# 分组聚合后绘图
df["月份"] = df.index.month
monthly = df.groupby("月份")[["销售额", "成本"]].sum()
monthly.plot(kind="bar", figsize=(10, 6), title="月度汇总")
plt.show()
有效可视化的原则
制作好的图表不仅是技术问题,更是设计问题。以下是几条核心原则:
1. 选择合适的图表类型
- 趋势变化 → 折线图
- 分类对比 → 柱状图/条形图
- 数据分布 → 直方图/箱线图
- 变量关系 → 散点图
- 构成比例 → 饼图/环形图/堆叠柱状图
2. 保持简洁
- 减少非必要元素:去掉多余的网格线、边框、图例
- 数据墨水比最大化:让数据本身占主导,减少装饰性元素
- 避免 3D 效果:3D 图表通常扭曲数据感知,除非真有必要
3. 注意色彩使用
- 有限调色板:不要用太多颜色,3-5 种通常足够
- 色盲友好:避免仅用红绿区分,使用色盲友好的调色板
- 一致性:跨图表保持相同类别的颜色一致
4. 标注要清晰
- 包含有意义的标题
- 轴标签要完整,标明单位
- 重要的数据点可以直接标注
- 图例放置要合理
5. 避免常见陷阱
# 错误示例:截断坐标轴
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# 错误:坐标轴不从 0 开始,夸大了差异
axes[0].bar(["A", "B"], [45, 50], color=["#2196F3", "#F44336"])
axes[0].set_ylim(40, 52)
axes[0].set_title("错误:坐标轴截断", color="red")
axes[0].set_ylabel("值")
# 正确:坐标轴从 0 开始
axes[1].bar(["A", "B"], [45, 50], color=["#2196F3", "#F44336"])
axes[1].set_ylim(0, 55)
axes[1].set_title("正确:从 0 开始")
axes[1].set_ylabel("值")
plt.show()
# 错误示例:饼图太多类别
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
many_categories = [f"类别 {i}" for i in range(12)]
many_values = np.random.randint(1, 20, 12)
# 错误:类别太多,饼图难以阅读
axes[0].pie(many_values, labels=many_categories, autopct="%1.1f%%")
axes[0].set_title("错误:饼图类别过多", color="red")
# 正确:改为柱状图
axes[1].bar(many_categories, many_values, color="steelblue")
axes[1].set_title("正确:柱状图展示多类别数据")
axes[1].tick_params(axis="x", rotation=45)
plt.tight_layout()
plt.show()
实战:综合可视化报告
# 生成综合数据集
np.random.seed(42)
n = 365
dates = pd.date_range("2024-01-01", periods=n, freq="D")
sales_data = pd.DataFrame({
"日期": dates,
"销售额": np.random.normal(3000, 500, n).clip(500, 6000),
"客流量": np.random.poisson(200, n),
"客单价": np.random.normal(15, 3, n).clip(5, 30),
"区域": np.random.choice(["北区", "南区", "东区", "西区"], n),
"类别": np.random.choice(["电子产品", "服装", "食品", "日用品"], n)
})
sales_data["月份"] = sales_data["日期"].dt.month
sales_data["周几"] = sales_data["日期"].dt.dayofweek
sales_data["周"] = sales_data["日期"].dt.isocalendar().week
# 创建多面板报告
fig = plt.figure(figsize=(16, 12))
gs = GridSpec(3, 4, figure=fig, hspace=0.3, wspace=0.3)
# 面板1:月度趋势
ax1 = fig.add_subplot(gs[0, :2])
monthly_sales = sales_data.groupby("月份")["销售额"].mean()
ax1.plot(monthly_sales.index, monthly_sales.values,
marker="o", linewidth=2, color="#2196F3")
ax1.set_title("月均销售额趋势", fontsize=13, fontweight="bold")
ax1.set_xlabel("月份")
ax1.set_ylabel("平均销售额")
ax1.grid(alpha=0.3)
# 面板2:区域对比
ax2 = fig.add_subplot(gs[0, 2])
region_data = sales_data.groupby("区域")["销售额"].sum()
colors = ["#2196F3", "#4CAF50", "#FF9800", "#F44336"]
ax2.pie(region_data, labels=region_data.index, autopct="%1.1f%%",
colors=colors, startangle=90)
ax2.set_title("区域销售额占比", fontsize=13, fontweight="bold")
# 面板3:周营业模式
ax3 = fig.add_subplot(gs[0, 3])
weekday_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
weekday_sales = sales_data.groupby("周几")["销售额"].mean()
ax3.bar(weekday_sales.index, weekday_sales.values, color="#9C27B0", alpha=0.8)
ax3.set_xticks(range(7))
ax3.set_xticklabels(weekday_names, rotation=45)
ax3.set_title("周内日均销售额", fontsize=13, fontweight="bold")
ax3.grid(axis="y", alpha=0.3)
# 面板4:销售额分布
ax4 = fig.add_subplot(gs[1, 0])
ax4.hist(sales_data["销售额"], bins=30, color="#2196F3",
edgecolor="white", alpha=0.7)
ax4.axvline(sales_data["销售额"].mean(), color="red",
linestyle="--", label=f'均值: {sales_data["销售额"].mean():.0f}')
ax4.axvline(sales_data["销售额"].median(), color="green",
linestyle="--", label=f'中位数: {sales_data["销售额"].median():.0f}')
ax4.set_title("销售额分布", fontsize=13, fontweight="bold")
ax4.set_xlabel("销售额")
ax4.legend()
ax4.grid(alpha=0.3)
# 面板5:客流量 vs 销售额
ax5 = fig.add_subplot(gs[1, 1])
ax5.scatter(sales_data["客流量"], sales_data["销售额"],
alpha=0.3, color="#4CAF50", s=10)
z = np.polyfit(sales_data["客流量"], sales_data["销售额"], 1)
p = np.poly1d(z)
ax5.plot(sales_data["客流量"].sort_values(),
p(sales_data["客流量"].sort_values()),
color="red", linestyle="--", linewidth=2)
ax5.set_title("客流量与销售额关系", fontsize=13, fontweight="bold")
ax5.set_xlabel("客流量")
ax5.set_ylabel("销售额")
ax5.grid(alpha=0.3)
# 面板6:类别表现
ax6 = fig.add_subplot(gs[1, 2:])
category_data = sales_data.groupby("类别")["销售额"].agg(["sum", "mean", "count"])
category_data = category_data.sort_values("sum", ascending=True)
ax6.barh(category_data.index, category_data["sum"], color="#FF9800", alpha=0.8)
for i, (idx, row) in enumerate(category_data.iterrows()):
ax6.text(row["sum"] + 10000, i, f'{row["sum"]:,.0f}',
va="center", fontsize=10)
ax6.set_title("各类别总销售额", fontsize=13, fontweight="bold")
ax6.set_xlabel("总销售额")
ax6.grid(axis="x", alpha=0.3)
# 面板7:周趋势(第2行和第3行组合)
ax7 = fig.add_subplot(gs[2, :])
weekly_sales = sales_data.groupby("周")["销售额"].sum()
ax7.plot(weekly_sales.index, weekly_sales.values,
color="#F44336", linewidth=2)
ax7.fill_between(weekly_sales.index, weekly_sales.values,
alpha=0.2, color="#F44336")
ax7.set_title("周销售额趋势", fontsize=13, fontweight="bold")
ax7.set_xlabel("周数")
ax7.set_ylabel("周销售额")
ax7.grid(alpha=0.3)
plt.suptitle("2024 年销售数据分析报告", fontsize=16, fontweight="bold", y=0.98)
plt.show()
print("报告生成完成!共包含 7 个可视化面板。")
下一步
入门篇的最后一篇文章到这里就结束了。你完成了从环境搭建到 Python 基础,再到 NumPy、Pandas 和数据可视化的完整入门学习。接下来的进阶篇将深入探讨更复杂的数据清洗技巧、时间序列分析、高级可视化以及机器学习基础。祝你在数据分析的道路上越走越远!