目录14

文章Matplotlib第 6 章

饼图

Matplotlib · 第 6 / 43 章

饼图 pie()

饼图是极坐标的:圆心是原点,每个扇区的角度 = 该值占总和的比例。它只回答一个问题——各部分占整体的多少。

Python20 行
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 归一化 关掉后可以画「半张饼」

最小示例

Python9 行
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:必须非负,负数直接报错

Python2 行
ax.pie([10, -5, 5])
# ValueError: Wedge sizes 'x' must be non negative values

正数不必和为 1,也不必和为 100,normalize=True 会自动归一。想检查归一后的比例,看返回值:

Python3 行
wedges, texts = ax.pie([35, 25, 20, 12, 8])
print(wedges[0].theta2 - wedges[0].theta1)
# 输出: 126.0     ← 35% × 360°

normalize=False 是另一种用法:只画占整体的一部分,剩下的留空。

Python2 行
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:把某块炸出来

长度必须等于扇区数,每个值是该扇区沿径向外移的距离(单位 = 半径)。

Python5 行
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 已经很夸张

explode 的四种取值:不炸、炸一块、全炸、大幅炸一块

Python2 行
ax.pie([1, 2, 3], explode=(0.1, 0.2))
# ValueError: 'explode' must be of length 'x', not 2

漏写一个 0 是最常见的错误,报错信息里直接给了两个长度,一看就知道差在哪。

labels / labeldistance / rotatelabels

Python6 行
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。旋转后标签会跟着扇区角度转,阅读方向不统一,所以只在标签确实很长时才用。

labeldistance 与 rotatelabels:默认、外推、旋转

autopct:扇区里显示什么

三种写法:

Python4 行
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。

返回值个数的坑:

Python2 行
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——不是因为饼图画错了,是解包数量不对。

autopct 不给、给整数、给整数并调 pctdistance

pctdistance:百分比显示在哪个半径上

默认 0.6,即半径 60% 处。饼图里不必改;环形图里必须改,否则数字会压在环上。

规律很简单:

环的内半径 = 1 - width
环的外半径 = 1
数字该落在  = (1 - width + 1) / 2 = 1 - width / 2
wedgeprops.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 点。

Python4 行
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 定起点,counterclock 定方向

国内做报表的惯例是 startangle=90, counterclock=False——从 12 点方向开始顺时针排,第一块落在右上象限,和 PPT 里手画的一致。

colors:不够时会循环,不报错

Python2 行
ax.pie([1, 2, 3], colors=["red", "green"])
# 3 个扇区,2 个颜色 → 红、绿、红(不报错,第三块又绕回红色)

这是最容易看走眼的一处:参数表里写「长度须 ≥ 扇区数」,实际是循环取色。多组饼图对比时,如果颜色列表少一个,两张图的配色会悄悄错位。

想用现成的色板:

Python3 行
colors = plt.get_cmap("Pastel1").colors[:5]     # 3.9+ 推荐写法
# 旧写法 plt.cm.get_cmap("Pastel1", 5) 已弃用
ax.pie(sizes, colors=colors)

textprops 与 autotexts:两层文字分开改

textprops 一次管所有文字(外标签 + 百分比),但它管不了「只改百分比颜色」。

Python9 行
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 全都能用。

Python10 行
# 加白描边:把相邻扇区分开
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="//"))

wedgeprops:描边、环形、细环、斜线填充

width 是环宽占半径的比例,不是内半径。width=0.5 表示从 0.5 到 1.0 这一段,中间 0.5 是空的。

radius / center / shadow

Python3 行
ax.pie(sizes, radius=0.6)          # 整体缩小(圆心不动,坐标范围跟着缩)
ax.pie(sizes, center=(2, 3))       # 圆心平移到 (2, 3)
ax.pie(sizes, shadow=True)         # 阴影(视觉过时,尽量别用)

radius 和 center 都会改 Axes 的坐标范围,这是饼图的一个副作用:

Python3 行
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 会把坐标轴框显示出来,只有在需要标注坐标系时才用:

Python3 行
ax.pie([1, 1], frame=True)
print(ax.axison)
# 输出: True

hatch 是顶层参数,等价于写进 wedgeprops:

Python2 行
ax.pie([1, 1], hatch="//")                          # 等价于
ax.pie([1, 1], wedgeprops=dict(hatch="//"))

**kwargs 里剩下的键会传给每个扇形多边形(Patch),所以 alpha、linestyle 这些也能直接写在外面,但混着写不如统一放进 wedgeprops 清楚。

参数说明

Matplotlib 饼图参数详解信息图

注意事项

  • 不写 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% 的扇形,这种情况用条形图。

综合示例:三张图覆盖常用组合

Python43 行
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()

三种常用组合:基础饼图、环形图、阴影缩小版