目录14
文章/Matplotlib/第 6 章
饼图
Matplotlib · 第 6 / 43 章
饼图 pie()
饼图是极坐标的:圆心是原点,每个扇区的角度 = 该值占总和的比例。它只回答一个问题——各部分占整体的多少。
Axes.pie(
x, # 各扇形的数值,不必和为 1(默认自动归一)
explode=None, # 每个扇区沿径向外移的距离,单位是「半径」
labels=None, # 扇区外侧的标签文字
colors=None, # 每个扇区的填充色;不够时循环取
autopct=None, # 扇区内显示什么;给值才返回 autotexts
pctdistance=0.6, # 百分比文字距圆心的距离(占半径比例)
shadow=False, # 阴影,视觉过时
labeldistance=1.1, # 外标签距圆心的距离(占半径比例)
startangle=0, # 第一个扇区的起始角度,度;90 = 12 点方向
radius=1, # 整体半径
counterclock=True, # 是否逆时针排列
wedgeprops=None, # 传给每个扇形多边形的属性字典(width 用来做环形)
textprops=None, # 传给所有文字对象的属性字典
center=(0, 0), # 圆心坐标
frame=False, # 是否显示坐标轴框
rotatelabels=False, # 标签是否随扇区角度旋转
normalize=True, # 是否把 x 归一到和为 1
hatch=None, # 填充图案,如 "//" ".." "xx"
)| 参数 | 一句话 | 默认值的影响 |
|---|---|---|
x |
各扇形数值 | 自动归一化,所以 [35,25,20,12,8] 和 [0.35,0.25,...] 效果一样 |
explode |
炸开距离 | 长度必须等于扇区数,否则报错 |
labels |
外标签 | 顺序与 x 一一对应 |
colors |
填充色 | 不够会循环,不会报错(容易看走眼) |
autopct |
百分比文字 | 为 None 时返回值只有 2 个元素 |
pctdistance |
百分比半径位置 | 环形图必须跟着 width 调 |
labeldistance |
外标签半径位置 | 1.1,标签重叠时往外推 |
startangle |
起始角度 | 0 = 3 点方向 |
counterclock |
排列方向 | True 逆时针 |
radius |
半径 | 与其他图叠加时用来缩小 |
wedgeprops |
扇形属性 | width < 1 就变成环形图 |
textprops |
文字属性 | 只管大小/颜色,不管位置 |
center |
圆心 | 移动饼图但不动坐标轴 |
frame |
坐标框 | False,饼图通常不要框 |
rotatelabels |
标签旋转 | 长标签挤在一起时打开 |
normalize |
归一化 | 关掉后可以画「半张饼」 |
最小示例
import matplotlib.pyplot as plt
sizes = [30, 20, 25, 25]
labels = ['A', 'B', 'C', 'D']
fig, ax = plt.subplots()
ax.pie(sizes, labels=labels)
ax.set_aspect("equal") # ★ 不写这句,非方形子图里饼会变成椭圆
plt.show()
ax.set_aspect("equal") 是饼图最容易漏的一句。pie() 自己不保证等比例,子图一旦不是正方形,圆就被压成椭圆——这是「我的饼图为什么是扁的」的唯一答案。
x:必须非负,负数直接报错
ax.pie([10, -5, 5])
# ValueError: Wedge sizes 'x' must be non negative values正数不必和为 1,也不必和为 100,normalize=True 会自动归一。想检查归一后的比例,看返回值:
wedges, texts = ax.pie([35, 25, 20, 12, 8])
print(wedges[0].theta2 - wedges[0].theta1)
# 输出: 126.0 ← 35% × 360°normalize=False 是另一种用法:只画占整体的一部分,剩下的留空。
ax.pie([0.3, 0.2], normalize=False) # 和 = 0.5,只画半圆
# 两个扇区覆盖 180°,右边一半空着注意关掉归一化后 sum(x) > 1 会报错:ValueError: Cannot plot an unnormalized pie with sum(x) > 1。
explode:把某块炸出来
长度必须等于扇区数,每个值是该扇区沿径向外移的距离(单位 = 半径)。
sizes = [35, 25, 20, 12, 8]
ax.pie(sizes, explode=(0.1, 0, 0, 0, 0)) # 只炸第一块
ax.pie(sizes, explode=(0.1,) * 5) # 全部炸开,缝隙均匀
ax.pie(sizes, explode=(0, 0, 0.3, 0, 0)) # 炸第三块,0.3 已经很夸张
ax.pie([1, 2, 3], explode=(0.1, 0.2))
# ValueError: 'explode' must be of length 'x', not 2漏写一个 0 是最常见的错误,报错信息里直接给了两个长度,一看就知道差在哪。
labels / labeldistance / rotatelabels
names = ['手机', '电脑', '平板', '耳机', '配件']
sizes = [35, 25, 20, 12, 8]
ax.pie(sizes, labels=names) # 默认 1.1
ax.pie(sizes, labels=names, labeldistance=1.35) # 往外推
ax.pie(sizes, labels=names, labeldistance=1.28, rotatelabels=True) # 沿径向旋转三个参数配合使用:长标签互相压住时,先加 labeldistance,还不够再开 rotatelabels。旋转后标签会跟着扇区角度转,阅读方向不统一,所以只在标签确实很长时才用。

autopct:扇区里显示什么
三种写法:
ax.pie(sizes, labels=labels) # 不显示
ax.pie(sizes, labels=labels, autopct="%d%%") # 35%
ax.pie(sizes, labels=labels, autopct="%1.1f%%") # 35.0%
ax.pie(sizes, labels=labels, autopct=lambda p: f"{p:.0f}%") # 也可以给函数%% 必须双写:第一个 % 开始格式说明,%% 才输出一个真的百分号。写成 "%d%" 会报 ValueError: incomplete format。
返回值个数的坑:
wedges, texts = ax.pie(sizes) # 2 个
wedges, texts, autotexts = ax.pie(sizes, autopct="%d%%") # 3 个给了 autopct 才有第三个返回值。写 wedges, texts = ax.pie(..., autopct=...) 会直接 ValueError: too many values to unpack——不是因为饼图画错了,是解包数量不对。

pctdistance:百分比显示在哪个半径上
默认 0.6,即半径 60% 处。饼图里不必改;环形图里必须改,否则数字会压在环上。
规律很简单:
环的内半径 = 1 - width
环的外半径 = 1
数字该落在 = (1 - width + 1) / 2 = 1 - width / 2wedgeprops.width |
环宽区间 | 合适的 pctdistance |
|---|---|---|
| 不设(实心饼) | 0 ~ 1 | 0.6(默认) |
| 0.5 | 0.5 ~ 1.0 | 0.75 |
| 0.3 | 0.7 ~ 1.0 | 0.85 |
startangle / counterclock:从哪起、往哪转
startangle 的单位是度,从 3 点方向开始逆时针量:0 = 3 点,90 = 12 点,180 = 9 点,270 = 6 点。
ax.pie(sizes, labels=labels, startangle=0) # 3 点起,逆时针
ax.pie(sizes, labels=labels, startangle=90) # 12 点起,逆时针
ax.pie(sizes, labels=labels, startangle=90, counterclock=False) # 12 点起,顺时针
ax.pie(sizes, labels=labels, startangle=180, counterclock=False) # 9 点起,顺时针
国内做报表的惯例是 startangle=90, counterclock=False——从 12 点方向开始顺时针排,第一块落在右上象限,和 PPT 里手画的一致。
colors:不够时会循环,不报错
ax.pie([1, 2, 3], colors=["red", "green"])
# 3 个扇区,2 个颜色 → 红、绿、红(不报错,第三块又绕回红色)这是最容易看走眼的一处:参数表里写「长度须 ≥ 扇区数」,实际是循环取色。多组饼图对比时,如果颜色列表少一个,两张图的配色会悄悄错位。
想用现成的色板:
colors = plt.get_cmap("Pastel1").colors[:5] # 3.9+ 推荐写法
# 旧写法 plt.cm.get_cmap("Pastel1", 5) 已弃用
ax.pie(sizes, colors=colors)textprops 与 autotexts:两层文字分开改
textprops 一次管所有文字(外标签 + 百分比),但它管不了「只改百分比颜色」。
wedges, texts, autotexts = ax.pie(
sizes, labels=labels, autopct="%1.1f%%",
textprops=dict(fontsize=10, color="#333333"), # 全部文字
)
for t in autotexts:
t.set_color("white") # 只改百分比
t.set_fontsize(9)
for t in texts:
t.set_fontweight("bold") # 只改外标签如果只想改一层,就抓返回值里的那一个列表。texts 是外标签,autotexts 是百分比。
wedgeprops:环形图就靠 width
wedgeprops 里的键原样传给每个扇形多边形,所以 edgecolor / linewidth / hatch / alpha 全都能用。
# 加白描边:把相邻扇区分开
ax.pie(sizes, wedgeprops=dict(edgecolor="white", linewidth=1.5))
# 环形图(甜甜圈):width < 1 就从圆心挖掉一块
ax.pie(sizes, autopct="%1.0f%%", pctdistance=0.75,
wedgeprops=dict(width=0.5, edgecolor="white", linewidth=1.5))
# 更细的环 + 斜线填充
ax.pie(sizes, wedgeprops=dict(width=0.3, edgecolor="black",
linewidth=0.8, hatch="//"))
width 是环宽占半径的比例,不是内半径。width=0.5 表示从 0.5 到 1.0 这一段,中间 0.5 是空的。
radius / center / shadow
ax.pie(sizes, radius=0.6) # 整体缩小(圆心不动,坐标范围跟着缩)
ax.pie(sizes, center=(2, 3)) # 圆心平移到 (2, 3)
ax.pie(sizes, shadow=True) # 阴影(视觉过时,尽量别用)radius 和 center 都会改 Axes 的坐标范围,这是饼图的一个副作用:
ax.pie([1, 1], center=(2, 3))
print(np.round(ax.get_xlim(), 2))
# 输出: [0.75 3.25] ← 圆心在 2,半径 1.25(含标签留白)所以在饼图上再叠加别的东西(比如中心写字)之前,得先知道范围被改成了什么。
frame、hatch 与 **kwargs
frame=True 会把坐标轴框显示出来,只有在需要标注坐标系时才用:
ax.pie([1, 1], frame=True)
print(ax.axison)
# 输出: Truehatch 是顶层参数,等价于写进 wedgeprops:
ax.pie([1, 1], hatch="//") # 等价于
ax.pie([1, 1], wedgeprops=dict(hatch="//"))**kwargs 里剩下的键会传给每个扇形多边形(Patch),所以 alpha、linestyle 这些也能直接写在外面,但混着写不如统一放进 wedgeprops 清楚。
参数说明

注意事项
- 不写
ax.set_aspect("equal"),饼就是椭圆。 这条排第一。 autopct里的%要双写:"%1.1f%%"。autopct决定返回值是 2 个还是 3 个。 解包数量对不上会报too many values to unpack。explode长度必须等于扇区数。colors不够会循环,不会报错,容易配错色。- 环形图的
pctdistance要落在环宽正中(1 - width/2)。 pie()会重置 Axes 的坐标范围,按半径 1 布置。- 饼图不适合超过 6 份,也不适合份额相近的对比——人眼比不出 5% 和 7% 的扇形,这种情况用条形图。
综合示例:三张图覆盖常用组合
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
sizes = [35, 25, 20, 12, 8] # 不要求和为 100,normalize=True 会自动归一
labels = ["手机", "电脑", "平板", "耳机", "配件"]
colors = plt.get_cmap("Pastel1").colors[:5]
# ---- (a) 基础饼图:顺时针 + 从 12 点起 ----
w1, t1, a1 = axes[0].pie(
sizes, labels=labels, autopct="%1.1f%%",
startangle=90, counterclock=False, colors=colors,
textprops=dict(fontsize=10),
)
for t in a1:
t.set_color("#333333")
axes[0].set_title("(a) 基础饼图(顺时针 + 从顶部起)")
# ---- (b) 环形图 + 分离扇形 ----
w2, t2, a2 = axes[1].pie(
sizes, labels=labels,
explode=(0.06, 0, 0, 0, 0),
autopct="%1.1f%%", startangle=90, counterclock=False, colors=colors,
wedgeprops=dict(width=0.55, edgecolor="white", linewidth=1.5),
pctdistance=0.75, # 0.55 的环宽 → 数字落在 0.725 附近
labeldistance=1.08,
)
axes[1].set_title("(b) 环形图(width=0.55)+ 分离扇形")
# ---- (c) 阴影 + 缩小半径 ----
w3, t3 = axes[2].pie(sizes, labels=labels, shadow=True, radius=0.85,
startangle=90, counterclock=False, colors=colors,
wedgeprops=dict(edgecolor="white", linewidth=1))
axes[2].set_title("(c) shadow=True + radius=0.85")
for ax in axes:
ax.set_aspect("equal") # ★ 保证圆是圆,不是椭圆
plt.tight_layout()
plt.show()