AI编程(六十四):饼图 — 把”地盘”分给你看

上一篇的柱状图比”多少”,今天换一种比”份额”的图——饼图。只引入一个新概念:占比展示:一个整体切成几块,谁占大头、谁是零头,”地盘”怎么分一眼看清。标签、百分比、分离扇形、甜甜圈一篇配齐,”几块才配用饼图”的三大纪律提前打疫苗。从此图表三兄弟到齐:折线管趋势、柱状管较量、饼图管份额。

一、真实场景:家庭会议的第二个问题

接着上一篇的家庭会议。柱状图把”买菜1860、下馆子920、零食水果610、交通430、水电燃气380″拍在了桌上,全家都服气:买菜是冠军。但下一个问题马上就来——”买菜占了咱家开销的多大一块?要是砍掉一半下馆子,能省出多少地盘?”

注意这个问题的问法变了:不再是”谁多谁少”(柱状图的活),而是“每一块占整体的多大份额”。全家总支出4200元是一个完整的”饼”,五个类别是切出来的五块:买菜44%、下馆子22%、零食水果14%、交通10%、水电燃气9%(凑一凑正好100%)。

这就是占比展示:把”部分与整体的关系”转成扇形的角度——一块饼转一圈是360度,也就是100%,谁的份额大,谁的”地盘”就大。什么时候用它?当你想回答的是“钱都花哪儿去了””时间都去哪儿了””用户都从哪儿来”这类”一个整体怎么分成几份”的问题,饼图就是默认答案。

今天只学这一个新概念,但纪律比前两篇都严——饼图是三兄弟里最容易被滥用、也最容易画成骗局的一个。学完这篇,”份额类”的数据归你管,”什么时候不许用饼图”你也门儿清。

二、饼图的”世界观”:它只讲”一个整体的分法”

先立规矩:饼图适合”部分占整体的份额”。判断口诀接着前两篇往下记:

  • 横轴说不了”之前之后”、能点名”谁跟谁”→ 柱状图(较量);
  • 数据随时间变化 → 折线图(趋势);
  • 各部分互斥、加起来正好等于一个整体 → 饼图(份额)。

第三条的两个条件缺一不可:”互斥”是说每块钱只归一块管(买菜的钱不能再算进下馆子),”等于整体”是说切完的饼不能缺角也不能多出一块。有一项不满足(比如”多选题投票”,一个人可以选好几项),饼图当场出局——这是坑点疫苗3的伏笔。

一场”分地盘”的故事是三个要素,画图就是把它们画醒目:

  1. 谁占大头:份额最大的是哪块?——最大的那块”扇形”;
  2. 占多少:大头是”半壁江山”还是”略微领先”?——百分比数字;
  3. 零头有几块:剩下的是三五块还是十几块?——扇形的数量和碎块。

所以”好的饼图”也不是画得漂亮,而是观众三秒钟能答出这三个问题。带着这个目标动手——每一步都在为三个要素服务。

三、动手:把地盘分给你看

第 1 步:第一张饼图 —— 谁占大头

# pie_basic.py — 第一张饼图:家庭月度支出占比
import matplotlib.pyplot as plt

# —— 中文字体(第61篇的坑点疫苗,直接复用)——
plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "SimHei", "WenQuanYi Zen Hei"]
plt.rcParams["axes.unicode_minus"] = False

categories = ["买菜", "下馆子", "零食水果", "交通", "水电燃气"]
amounts = [1860, 920, 610, 430, 380]

plt.pie(amounts, labels=categories)
plt.title("11月家庭支出占比")
plt.savefig("spending_pie.png", dpi=150, bbox_inches="tight")
plt.show()

plt.pie(数值, labels=类别) 就是饼图的全部骨架——matplotlib 会自动把数值换算成角度(4200元转一圈360度,1860元就是1860/4200×360≈159度)。但素饼信息量太少,”占多少”还得读者自己估。手艺人从第二步开始往里加料:

  • 饼图没有 x/y 轴——它不需要坐标轴,plt.grid 在这儿也无用武之地(网格线是给”对着轴读数”的图准备的);
  • 数据必须都是正数:负数或零没法切”地盘”,matplotlib 会直接报错或画出怪东西;
  • 数值自动归一化:你喂金额、喂数量都行,matplotlib 只关心”各块的相对大小”,最后拼成一个整圆。

第 2 步:标签和百分比 —— 让每块自己报家门

# pie_labels.py — 带百分比标签的饼图
import matplotlib.pyplot as plt

plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "SimHei", "WenQuanYi Zen Hei"]
plt.rcParams["axes.unicode_minus"] = False

categories = ["买菜", "下馆子", "零食水果", "交通", "水电燃气"]
amounts = [1860, 920, 610, 430, 380]
colors = ["#d9534f", "#f0ad4e", "#5bc0de", "#5cb85c", "#999999"]

wedges, texts, autotexts = plt.pie(
    amounts,
    labels=categories,
    colors=colors,
    autopct="%1.1f%%",      # 每块自动标百分比,保留1位小数
    pctdistance=0.75,       # 百分比放在扇形内部、离圆心75%半径处
    labeldistance=1.08,     # 类别名放在圆外8%处
    startangle=90,          # 从12点钟方向开始切
    counterclock=False      # 顺时针切
)

# 百分比文字加粗、白色,在彩色扇形上更醒目
for t in autotexts:
    t.set_fontsize(11)
    t.set_fontweight("bold")
    t.set_color("white")

plt.title("11月家庭支出占比")
plt.savefig("spending_pie_labels.png", dpi=150, bbox_inches="tight")
plt.show()

这一段的手艺全在参数上,逐个拆:

  • autopct="%1.1f%%":自动百分比的格式串。%1.1f 是”保留1位小数的浮点数”,后面的 %% 是转义出来的百分号。想取整就写 "%1.0f%%";
  • pctdistance / labeldistance:百分比在内、类别名在外,各占一圈,互不打架。类别名永远建议放圆外(>1.0),圆内放不下长名字;
  • startangle=90 + counterclock=False:从12点方向顺时针切。这是约定俗成的读图习惯——最大块从12点起步,读者从”正上方”开始顺时针读份额;
  • wedges, texts, autotexts 三兄弟:plt.pie 返回三个列表——扇形对象、类别文字、百分比文字。拿到它们就能逐个微调(字号、颜色、加粗),这是”跟AI说需求”时它最常动的地方。

百分比四舍五入后加起来可能是 99.9% 或 100.1%,这是正常的(各块自己四舍五入的误差),但对外的正式材料里要心里有数——坑点疫苗4细说。

第 3 步:分离扇形 + 甜甜圈 —— 强调和颜值

想让”买菜”这块单独站出来强调,或者把饼画得时髦一点,两招搞定:

# pie_explode_donut.py — 分离扇形 + 甜甜圈
import matplotlib.pyplot as plt

plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "SimHei", "WenQuanYi Zen Hei"]
plt.rcParams["axes.unicode_minus"] = False

categories = ["买菜", "下馆子", "零食水果", "交通", "水电燃气"]
amounts = [1860, 920, 610, 430, 380]
colors = ["#d9534f", "#f0ad4e", "#5bc0de", "#5cb85c", "#999999"]

# 分离量:每块往外挪多少(0=不挪),只把冠军"买菜"拽出来
explode = [0.06, 0, 0, 0, 0]

wedges, texts, autotexts = plt.pie(
    amounts,
    labels=categories,
    colors=colors,
    explode=explode,
    autopct="%1.1f%%",
    pctdistance=0.78,
    startangle=90,
    counterclock=False,
    wedgeprops={"width": 0.42, "edgecolor": "white", "linewidth": 2}  # 变甜甜圈
)

# 甜甜圈中间的"结论字"
total = sum(amounts)
plt.text(0, 0, f"总支出n{total} 元", ha="center", va="center",
         fontsize=14, fontweight="bold")

plt.title("11月家庭支出占比(冠军:买菜)")
plt.savefig("spending_donut.png", dpi=150, bbox_inches="tight")
plt.show()

两处新面孔:

  • explode=[0.06, 0, 0, 0, 0]:分离扇形。列表和数据一一对应,数值是”往外挪的半径比例”。只分离1~2块,全炸开就成了碎盘子(坑点疫苗2的邻居);
  • wedgeprops={"width": 0.42, ...}:width 是扇形的”环宽”,小于 1 就从实心饼变成甜甜圈。中间空出来的圆心正好写结论(总额、核心数字)——这是甜甜圈比实心饼多出来的地盘,别浪费。

白色 edgecolor 给每块扇形描个白边,块与块的边界立刻清爽——这是免费的可读性提升。

第 4 步:小类合并”其他” —— 饼图的自我修养

真实数据很少只有5块。类别一多(10块、15块),饼图就碎成调色板事故。标准解法是把小份额合并成”其他”:

# pie_others.py — 小类合并"其他"
import matplotlib.pyplot as plt

plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "SimHei", "WenQuanYi Zen Hei"]
plt.rcParams["axes.unicode_minus"] = False

# 11月的完整账目:8个类别
detail = {
    "买菜": 1860, "下馆子": 920, "零食水果": 610, "交通": 430,
    "水电燃气": 380, "话费网费": 180, "日用品": 150, "其他零碎": 90,
}

# —— 份额不足5%的小类合并进"其他" ——
total = sum(detail.values())
big = {k: v for k, v in detail.items() if v / total >= 0.05}
small = {k: v for k, v in detail.items() if v / total < 0.05}
big["其他"] = sum(small.values())

labels = list(big.keys())
sizes = list(big.values())
colors = ["#d9534f", "#f0ad4e", "#5bc0de", "#5cb85c", "#999999", "#c9c9c9"]

wedges, texts, autotexts = plt.pie(
    sizes, labels=labels, colors=colors,
    autopct="%1.1f%%", startangle=90, counterclock=False,
    wedgeprops={"width": 0.42, "edgecolor": "white", "linewidth": 2}
)
for t in autotexts:
    t.set_fontweight("bold")
    t.set_color("white")

plt.title("11月家庭支出占比(其他 = 话费网费+日用品+其他零碎)")
plt.savefig("spending_pie_others.png", dpi=150, bbox_inches="tight")
plt.show()

两个要点,一个都不能省:

  1. “其他”的成分必须写明。标题或图注里注明”‘其他’包含话费网费、日用品、其他零碎”——把小类藏进黑箱是数据欺骗,不是简化;
  2. “其他”本身也不能太大。如果合并完”其他”变成了第二大块,说明这个数据压根不该用饼图(碎块太多),该换横向条形图——每类一根,谁也不用合并。这就是”饼图的平替”:份额想逐项比大小,条形图永远比饼图准(人眼比长度比比角度准得多,第63篇讲过)。

四、改进:跟AI一起升级

把上面的 pie_basic.py 发给 AI,这次的需求往”能进汇报PPT”上提:

“这是我家庭月度支出的饼图(代码如下)。想升级成能放进汇报的版本:1)最大一块分离出来强调;2)每块标百分比,白色加粗;3)图中间写总支出;4)帮我检查这份数据用饼图是否合适。请先给方案说明每处改动的意图,确认后再给代码。”

它的方案里会冒出三个新面孔,先混脸熟:

  • explode 列表:分离强调的标准做法——和数据等长的列表,只给要强调的块写非零值;
  • wedgeprops 字典:扇形的”装修包”——环宽、边框、透明度都从这儿进;
  • ax.set_aspect("equal")(或 plt.axis("equal")):强制坐标轴等比例。matplotlib 的饼图默认是正圆,但一遇上标题、图例挤压画布就可能被拉成椭圆——椭圆的”角度”就不准了,这条几乎是 AI 必加的保险,值得自己记住。

逐条实现、逐条跑。重点不是它给了什么代码,是你能说出”这处改动让份额的哪个要素更清楚了”——说不出,就说明改动是花架子。另外让它回答第4问(数据适不适合饼图),这是今天最值钱的一问。

五、四个必踩的坑(先打疫苗)

  1. 块数超过6块 = 饼图当场出局。 扇形一多,颜色分不清、标签挤成粥、读者还要来回对照图例——三秒原则直接破产。铁律:超过6块,要么合并小类成”其他”(写明成分),要么换横向条形图。 条形图不需要合并也不需要图例来回跳,份额逐项比大小它更称职。
  1. 3D饼图、渐变、阴影 = 占比骗局。 3D 透视让”前面”的扇形显得大、”后面”的显得小——同样 20% 的两块,画出来能差出一倍的视觉面积;渐变和阴影让扇形的边界发虚,角度判断雪上加霜。饼图的美是2D 平涂 + 清晰边界 + 白描边,花活留给海报,不留给数据。(甜甜圈不算花活——环宽不改变角度,放心用。)
  1. 数据不满足”互斥+等于整体”就硬画。 多选题投票(一人可选多项)、”想去的地方”(可以列三个)——各项加起来超过100%,画饼就是自欺欺人。还有一种隐蔽的:份额彼此太接近(33%、32%、35%),人眼对角度的判断是所有视觉属性里最差的,三块看起来一样大,饼图等于白画——这种”势均力敌”的较量数据,用柱状图(第63篇)把差距拉直了看。
  1. 百分比四舍五入的”消失的一块”。 各块 %.1f%% 四舍五入后总和可能 ≠ 100%(99.9% 或 100.1% 都正常),对外材料要心里有数:要么注明”因四舍五入,合计可能不等于100%”,要么把最大块的尾数调平。另外起始角随手乱设(比如从3点钟方向、逆时针切)会让读者的阅读顺序和你的叙事顺序打架——startangle=90, counterclock=False 加最大块放首位,是份额图的”排队礼仪”。

六、跟AI协作的正确姿势(份额图特供版)

  1. 让 AI 当”读者”答题。 把图发给它,问三个问题:”哪块最大?占多少?碎块多不多?”它答不全,说明图没讲清份额,改图比改问法重要。
  1. 把”整体的定义”告诉它。 这个”饼”是什么整体?(全家总支出?全部用户?24小时?)各块互斥吗?合计等于整体吗?——份额图的所有纪律都从”整体”这两个字来,AI 拿不到这个前提,给的图就可能在讲一个不存在的故事。
  1. 让它当”图形选择的参谋”。 “这份数据用饼图合适吗?块数几块?有没有相近的份额?”它答得上”6块以内、互斥、份额拉开才用饼;否则条形图平替”,你就多了一个靠谱的参谋——这句话也是验收它给的图的标准。

七、今日小练习

练习1:画出你自己的份额饼图。 找一组”部分-整体”数据(月度开销、一天时间去向、手机电量去向都行),画带百分比标签的饼图:autopct="%1.1f%%"、最大一块 explode 分离、startangle=90 顺时针(提示:先自检三要素——谁占大头、占多少、零头几块;再自检”互斥+等于整体”两个前提成立吗?)。

练习2:做个甜甜圈。 把练习1的图改成甜甜圈(wedgeprops={"width": 0.42}),圆心写上总额或一句结论,白色描边分块(提示:plt.text(0, 0, ...) 在圆心写字;饼图容易被画布挤压成椭圆,加 plt.axis("equal") 保险)。

练习3:给碎块做”体检”。 造一份 8~10 个类别的数据,先硬画一张饼图感受”碎盘子”,再用合并法把它收成 ≤6 块(”其他”写明成分),最后用横向条形图画同一份数据,对比哪张图”谁大谁小”看得更准(提示:if v / total >= 0.05 筛大类;plt.barh 画条形图平替;自问:人眼比长度还是比角度更准?)。

下篇预告

下一篇(第65篇):散点图 — 看两件事有没有”默契”。 三兄弟讲的都是”一个变量”的故事,散点图第一次让两个变量同台:锻炼多的人是不是睡得香?广告花得多是不是卖得多?——”相关性”是数据分析从”描述”走向”发现”的第一步。下一篇学散点图的读法、回归线的初见,以及”相关≠因果”这个必须刻在脑门上的铁律。