AI编程(六十三):柱状图 — 把对比拍在桌上

上一篇的折线图讲”变化”,今天换一种会”拍桌子”的图——柱状图。只引入一个新概念:对比展示:谁多谁少、差距多大、排名如何,柱状图用”柱子的长短”直接把结论拍在桌上。分组柱、堆叠柱、横向条形图、数值标签一篇配齐,”柱子从哪儿起跑才不骗人”的头号骗局提前打疫苗。从此你的”图表兵器库”一攻一守:折线管趋势,柱状管较量。

一、真实场景:月底家庭会议上的”较量”

接着上一篇的账本开家庭会议。你把11月的支出按类别汇总出来了:买菜1860、下馆子920、零食水果610、交通430、水电燃气380(元)。要回答的问题很直接:”钱主要花哪儿了?哪块能省?”

这五个类别——买菜、下馆子、零食水果、交通、水电燃气——没有先后顺序,上一篇的折线图在这儿彻底没戏(还记得坑点疫苗3吗:横轴说不了”之前之后”)。你需要的是”较量”:五根柱子往台上一站,长短一目了然,冠军(买菜)和季军(零食水果)的差距直接用眼睛”量”出来。

这就是对比展示:把数值大小转成柱子的长度,让人眼来做裁判。为什么这么有效?因为人眼对”长度”的判断是所有视觉属性里最准的——比面积准、比角度准、比颜色深浅准得多。所以凡是”几个选手比成绩”的场合(品类对比、部门对比、月度对比、排名),柱状图都是默认答案。

今天只学这一个新概念,但柱状图的花样比折线图还多:竖着比、横着比、并排比、摞起来比。学完这篇,”较量类”的数据就全归你管了。

二、柱状图的”世界观”:它只讲一种故事

先立规矩:柱状图适合”分类之间的较量”。横轴是”选手”(类别),纵轴是”成绩”(数值),柱子的长短就是名次。判断口诀和上一篇正好互补:横轴说不了”之前之后”、但能点名”谁跟谁”,就该用柱状图。

一场较量的故事也是三个要素,画图就是把它们画醒目:

  1. 谁最高:冠军是哪一项?——最长的那根柱子;
  2. 差多少:冠军和亚军差距是碾压还是险胜?——柱子的”身高差”;
  3. 排第几:整体座次怎么排?——柱子从高到低的队形。

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

三、动手:把较量画成故事

第 1 步:第一张柱状图 —— 冠军是谁

# bar_compare.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]

bars = plt.bar(categories, amounts, color="#5bc0de", width=0.6)

# —— 数值标签:让每根柱子自己报数 ——
for bar in bars:
    height = bar.get_height()
    plt.text(bar.get_x() + bar.get_width() / 2, height + 20,
             f"{height} 元", ha="center", fontsize=10)

plt.title("11月家庭支出对比")
plt.ylabel("金额(元)")
plt.ylim(0, max(amounts) * 1.15)   # 顶部留15%空间,数值标签才不顶破房顶
plt.grid(axis="y", linestyle="--", alpha=0.4)
plt.savefig("spending_bar.png", dpi=150, bbox_inches="tight")
plt.show()

plt.bar(类别, 数值) 就是柱状图的全部骨架,但今天的手艺都藏在细节里:

  • width=0.6:柱子占”格子”的六成。太细像火柴棍,太胖挤成墙——0.5~0.7 之间最顺眼;
  • 数值标签的三件套:bar.get_height() 拿高度(就是数值)、bar.get_x() + bar.get_width() / 2 算柱子的水平中心、ha="center" 居中对齐。height + 20 是”比柱顶高 20″——标签浮在柱子头上,不压柱子;
  • ylim(0, max * 1.15):纵轴上限抬高 15%,给标签留出头顶空间。注意起点牢牢钉在 0——这是今天头号坑点的伏笔,坑点疫苗1专门讲;
  • grid(axis="y"):只画横向网格线。柱状图的”读数方向”是纵向的,竖网格线纯属干扰。

第 2 步:分组柱状图 —— 两组选手同台较量

一根柱子讲”单项成绩”,两根柱子并排讲”对手戏”。最经典的还是收支对比——但这次不是看走势(那是折线图的活),是逐月”硬碰硬”:

# grouped_bar.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

months = ["6月", "7月", "8月", "9月", "10月", "11月"]
income   = [8000, 8000, 8500, 8000, 9200, 8800]
spending = [4210, 3980, 4520, 4105, 4503, 4780]

x = list(range(len(months)))   # 站位表:0,1,2,3,4,5
width = 0.36

plt.bar([i - width/2 for i in x], income,   width=width, label="收入", color="#5bc0de")
plt.bar([i + width/2 for i in x], spending, width=width, label="支出", color="#d9534f")

plt.xticks(x, months)   # 把站位数字换回月份标签
plt.title("下半年收支同台对比")
plt.ylabel("金额(元)")
plt.legend()
plt.grid(axis="y", linestyle="--", alpha=0.4)
plt.savefig("income_vs_spending_bar.png", dpi=150, bbox_inches="tight")
plt.show()

分组柱的心法一句话:两根柱子”夹着”刻度线站。x 是每个刻度的站位,收入柱往左挪半个身位(i - width/2),支出柱往右挪半个身位(i + width/2),两根柱子加起来正好一个 width×2 的身位包住刻度。plt.xticks(x, months) 把 0、1、2 换回”6月、7月”。

留意这里”反着用”了上一篇的结论:同样的收支数据,折线图讲”逐月走势”,柱状图讲”逐月差距”——想知道”哪个月存得最多”看柱状图,想知道”支出是不是在爬坡”看折线图。图没有好坏,只有”想讲哪个故事”。

第 3 步:横向条形图 —— 长标签和排名的救星

类别名字一长(”孩子暑期兴趣班”七个字),x 轴标签就开始叠罗汉。这时候把整张图横过来——纵向排类别、横向伸柱子:

# hbar.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

# 已经按金额从大到小排好——冠军在最上面
projects = ["车险续保", "阳台防水补漏", "孩子暑期兴趣班", "全家体检", "换洗衣机"]
cost = [3600, 3200, 2800, 2400, 2100]

bars = plt.barh(projects, cost, color="#f0ad4e", height=0.55)

for bar in bars:
    w = bar.get_width()
    plt.text(w + 40, bar.get_y() + bar.get_height() / 2,
             f"{w} 元", va="center", fontsize=10)

plt.title("年度家庭大额开支排名")
plt.xlabel("金额(元)")
plt.xlim(0, max(cost) * 1.18)   # 横轴留空间给标签
plt.grid(axis="x", linestyle="--", alpha=0.4)
plt.savefig("big_spending_rank.png", dpi=150, bbox_inches="tight")
plt.show()

三处镜像变化:plt.barh(h=horizontal)画横向柱;数值标签用 bar.get_width() 取长度、va="center" 垂直居中;网格改 axis="x"。排序是我手工排好的——projects 和 cost 按金额降序写死,这比让程序排更可控(真要程序排,sorted(zip(cost, projects), reverse=True) 一把梭,第8篇列表的手艺)。

横向条形图还有个隐藏福利:它天然适合”排名”叙事。从上往下读就是从冠军读到末位,读者的阅读顺序和你讲故事的顺序完全一致。

第 4 步:堆叠柱状图 —— 总量里谁的”地盘”大

最后一招:把一根柱子拆成几段摞起来,既看”总量”,又看”内部构成”:

# stacked_bar.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

months = ["9月", "10月", "11月"]
food      = [2100, 2250, 2470]   # 吃饭(买菜+下馆子)
living    = [810, 790, 810]     # 居住(水电燃气+物业)
fun       = [760, 890, 980]     # 娱乐零食
transport = [430, 410, 430]     # 交通

plt.bar(months, food, label="吃饭", color="#d9534f")
plt.bar(months, living, bottom=food, label="居住", color="#5bc0de")
plt.bar(months, fun,
        bottom=[a + b for a, b in zip(food, living)],
        label="娱乐零食", color="#f0ad4e")
plt.bar(months, transport,
        bottom=[a + b + c for a, b, c in zip(food, living, fun)],
        label="交通", color="#5cb85c")

plt.title("近三个月支出结构对比")
plt.ylabel("金额(元)")
plt.legend()
plt.grid(axis="y", linestyle="--", alpha=0.4)
plt.savefig("spending_stack.png", dpi=150, bbox_inches="tight")
plt.show()

魔法全在 bottom=:每一段不从 0 起跑,而是从”下面所有段的总和”起跑。zip + 列表推导式求和是第8篇和第15篇的老手艺——[a + b for a, b in zip(food, living)] 就是逐月算”吃饭+居住”的底座。每段的颜色要明显区隔(四段四色,再多就是调色板事故,坑点疫苗4)。

到这里,柱状图的四件套齐了:竖着比(bar)、并排比(grouped)、横着比(barh)、摞着比(stacked)。

四、改进:跟AI一起升级

把上面的 bar_compare.py 发给 AI,这次的需求往”能开会”上提:

“这是我的家庭月度支出柱状图(代码如下)。想升级成能放进家庭会议的版本:1)加一条 4000 元的’单项预算线’,超过预算的柱子标红、没超的保持蓝色;2)柱子按金额从高到低排,让’冠军’一眼可见;3)标题直接把结论写出来(比如’买菜是绝对大头’)。请先给方案说明每处改动的意图,确认后再给代码。”

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

  • plt.axhline(y=4000, color="red", linestyle="--"):一条水平参考线横穿全图——预算线、目标线、平均线全是这招(对应 axvline 画竖线);
  • 条件配色:colors = ["#d9534f" if v > 4000 else "#5bc0de" for v in amounts]——把颜色变成”达标/超标”的信号灯,第15篇列表推导式直接上岗;
  • sorted 排序后重画:先把 (类别, 数值) 对子排好序再喂给 plt.bar,队形自动从高到低。

逐条实现、逐条跑。重点不是它给了什么代码,是你能说出”这处改动让较量的哪个要素更清楚了”——说不出,就说明改动是花架子。

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

  1. y轴不从0起跑 = 柱状图的头号骗局。 柱子的全部说服力来自”长度与数值成正比”。把纵轴起点从 0 挪到 350,380 和 430 两根柱子立刻变成”一倍差距”的假象——同样的数据,图能”讲”出完全相反的故事。铁律:柱状图的数值轴必须从 0 开始,一秒都不能含糊。(折线图可以例外——线看的是走势不是长度——柱状图绝对不行。)如果差距小到从 0 看不出来,那是数据本身差距就小,诚实的表达是标注数值,不是掰弯尺子。
  1. 3D 柱、阴影、渐变花活 = 误差制造机。 3D 透视让柱顶和刻度对不上(前面的柱子还挡后面的),阴影让柱子边界发虚,渐变让”长度”的判断起点模糊。柱状图的美是2D 平涂 + 清晰边界,花活留给海报,不留给数据。
  1. 类目一多,柱子挤成”梳子”。 超过 10 根柱子,x 轴标签互相踩、读者对比疲劳。三招救场:改横向条形图、合并小类为”其他”(但必须注明”其他”包含什么,藏着掖着就是骗人)、只取 Top N(标题写明”仅列前五”)。
  1. 堆叠柱只管”总量”,不管”内部对比”。 每段的起点忽上忽下(除了顶段和底段),眼睛根本没法比较”娱乐零食三个月各多少”。想比内部各段的变化就换分组柱;堆叠柱只回答两个问题——总量谁大、大块地盘谁占得多。另外每根柱子的分段别超过 5 种颜色,再多连图例都记不住。

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

  1. 让 AI 当”读者”答题。 把图发给它,问三个问题:”冠军是谁?差距多大?座次怎么排?”它答不全,说明图没讲清较量,改图比改问法重要。
  1. 把”较量的规则”告诉它。 有没有预算线?要不要排名?跟谁比(自己上月、同行、目标)?规则不同,AI 给的参考线、排序、配色方案完全不同。
  1. 让它解释”为什么是柱状图”。 “这份数据为什么用柱状图不用饼图或折线图?”——图形选择的理由比代码更值钱。它答得上”分类较量用柱、随时间变化用折线、占比构成才轮到饼图”,你就多了一个靠谱的参谋。

七、今日小练习

练习1:画出你自己的分类较量图。 找一组分类数据(月度开销、各APP使用时长、一周家务分配都行,5~8 类),画柱状图:数值标签、纵轴从 0、超过某条”预算线”的柱子换个颜色(提示:plt.bar + for bar in bars 标数值;plt.axhline 画预算线;条件配色用列表推导式 [... if v > 阈值 else ... for v in 数值])。

练习2:来一场双人对决。 用分组柱状图对比两组数据(收入vs支出、计划vs实际都行),要求:两根柱子夹着刻度站、双色、图例、plt.xticks 换回文字标签(提示:x = list(range(n)) 站位表,i - width/2 和 i + width/2 各占半边)。

练习3:排出你的 Top 5。 用横向条形图做一张排名图,类别名故意用长名字(7 个字以上),按数值降序排列,每根柱子标数值(提示:plt.barh + va="center" 的数值标签;排序在写数据时就排好,或用 sorted(zip(数值, 类别), reverse=True);画完自问三要素:谁最高、差多少、排第几,三秒答得出吗?)。

下篇预告

下一篇(第64篇):饼图 — 把”地盘”分给你看。 柱状图比”多少”,饼图比”份额”:一块饼里谁占大头、谁是零头。下一篇学饼图的三大纪律——几块才配用饼图、标签和百分比怎么放、什么时候坚决不用饼图(以及它的平替)。三类图学完,”数据开口说话”的第一阶段就毕业了。