AI编程(六十八):数据+图表 — pandas+matplotlib,让程序自动出图

前面画图的数据都是手敲的列表,真实工作里数据躺在 CSV 和 Excel 里。今天把第31篇的 pandas 和这几篇的 matplotlib 接上:读表、算数、画图一条龙。只引入一个新概念:df.plot()——DataFrame 自带的”一键出图”,数据一变图就更新,从”画图的人”升级成”出图的流水线”。

一、真实场景:手敲数据画图,画到第三次就崩溃

季度汇报要出三张图:全年月度销售趋势、各品类销量对比、各渠道占比。

你的做法:打开 CSV,把十二个月的销售额一个一个抄进 Python 列表,画完趋势图;再打开另一张表,抄品类数据,画柱状图;再抄渠道数据,画饼图。三张图抄了三轮数,抄到第二轮开始眼花,把 6 月的 5.2 万抄成 52,图上出现一根冲天柱,被老板当场点名:”这数据靠谱吗?”

更崩溃的是下周:数据更新了,三张图全部作废,重新抄、重新画。你成了”人肉数据搬运工”,画图十分钟、抄数两小时。

同事的做法:写好一个脚本,pd.read_csv("销售.csv") 把整张表一口吞下,df.plot(...) 一行出图。数据更新了?换掉 CSV 重跑一次,三张图十秒钟全部刷新,没有抄数环节,自然也没有抄错环节。

这就是今天要接上的那根管子:pandas 负责吃数据、算数据,matplotlib 负责画图,中间用 df.plot() 直连。两个你都学过了,今天只是把它们焊在一起——但焊上之后,你的工作方式会发生质变。

二、新概念:df.plot() —— DataFrame 自带的”一键出图”

先回顾一下两边的老朋友:

  • pandas(第31篇):pd.read_csv() 读表,得到一个 DataFrame——你可以把它想象成”程序里的 Excel 表格”,有行有列,能筛选能算数;
  • matplotlib(第61-67篇):plt.plot()、plt.bar() 画图,但数据要自己整理成一个个列表传进去。

以前的问题是”翻译”:DataFrame 里的数据要拆成 months = [...]、sales = [...] 这样的列表才能喂给 matplotlib,表有 50 列就得拆 50 次,拆错了图就错。

df.plot() 是 DataFrame 自带的画图方法——它跳过”拆列表”这一步,直接把表格的列喂给 matplotlib。你说”用’月份’当横轴、’销售额’当纵轴画折线图”,它就画好了:

# plot_basic.py — df.plot() 第一眼:读表、出图,一气呵成
import pandas as pd
import matplotlib.pyplot as plt

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

# 先造一份 CSV(真实工作里这一步换成你自己的文件)
df_demo = pd.DataFrame({
    "月份": ["1月", "2月", "3月", "4月", "5月", "6月"],
    "销售额": [42, 38, 45, 50, 48, 52],
    "成本": [30, 28, 32, 35, 33, 36],
})
df_demo.to_csv("sales.csv", index=False, encoding="utf-8-sig")

# 第1步:pandas 吃下整张表
df = pd.read_csv("sales.csv")

# 第2步:一行出图 —— x、y 指名道姓,kind 说明画什么图
ax = df.plot(x="月份", y=["销售额", "成本"], kind="line", marker="o")

# 第3步:剩下的还是熟悉的 matplotlib 手艺
ax.set_title("上半年销售与成本趋势(万元)")
ax.set_ylabel("金额(万元)")
plt.savefig("sales_trend.png", dpi=150, bbox_inches="tight")
plt.show()

三个关键点,看懂就入门了:

  1. df.plot(x="月份", y=["销售额", "成本"], kind="line") —— x 是横轴用哪列,y 是纵轴用哪列(传列表就是多条线一起画,图例自动标好”销售额””成本”),kind 是图的类型:"line" 折线、"bar" 柱状、"pie" 饼、"scatter" 散点——和你学过的四种图一一对应;
  2. ax = df.plot(...) 的返回值还是那个老朋友 ax —— 也就是说 df.plot() 画完,你照样能用 ax.set_title()、ax.set_ylim() 这些 matplotlib 手艺继续加工。它不是新画图库,只是给 matplotlib 装了个”自动喂数据”的传送带;
  3. to_csv(..., index=False, encoding="utf-8-sig") —— 造示例文件用的,index=False 不写行号列,utf-8-sig 让 Excel 打开不乱码(这个坑第25篇打过疫苗)。

一句话总结:pandas 管”数据从哪来、怎么算”,df.plot() 管”把算好的数据递到 matplotlib 手上”。今天唯一的新概念就是这条传送带。

三、动手:从一张 CSV 到三张图的流水线

第 1 步:先把”吃数据”跑通

真实场景里你手里是一份销售明细 CSV。我们造一份带品类、渠道、金额的明细表,再用 pandas 算出汇报要的三组数:

# pipeline_data.py — 第一段流水线:读明细、算汇总
import pandas as pd

# 造一份销售明细(真实工作里换成 pd.read_csv("你的文件.csv") 即可)
detail = pd.DataFrame({
    "月份": ["1月", "2月", "3月", "4月", "5月", "6月"] * 3,
    "品类": ["数码"] * 6 + ["家居"] * 6 + ["服饰"] * 6,
    "渠道": ["线上", "门店", "线上", "门店", "线上", "门店"] * 3,
    "销售额": [42, 38, 45, 50, 48, 52,
              25, 28, 30, 33, 31, 36,
              18, 20, 22, 25, 24, 28],
})
detail.to_csv("sales_detail.csv", index=False, encoding="utf-8-sig")

# 读回来(从这里开始就是你日常的真实代码)
df = pd.read_csv("sales_detail.csv")

# 三组数:月度趋势、品类对比、渠道占比
by_month = df.groupby("月份", as_index=False)["销售额"].sum()
by_category = df.groupby("品类", as_index=False)["销售额"].sum()
by_channel = df.groupby("渠道", as_index=False)["销售额"].sum()

print(by_month)
print(by_category)
print(by_channel)

groupby 是第33篇学过的”分班报数”:按月份分组求和,就得到月度趋势;按品类分组求和,就得到品类对比。以前这些数算完要手工抄进画图代码,今天不用了——算完的 DataFrame 直接画。

第 2 步:三组数,一行一张图

# pipeline_plot.py — 第二段流水线:算完就画,中间零搬运
import pandas as pd
import matplotlib.pyplot as plt

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

df = pd.read_csv("sales_detail.csv")

by_month = df.groupby("月份", as_index=False)["销售额"].sum()
by_category = df.groupby("品类", as_index=False)["销售额"].sum()
by_channel = df.groupby("渠道", as_index=False)["销售额"].sum()

fig, axs = plt.subplots(1, 3, figsize=(13, 4.5), constrained_layout=True)

# 折线:月度趋势
by_month.plot(x="月份", y="销售额", kind="line", marker="o",
              color="#4878A8", legend=False, ax=axs[0])
axs[0].set_title("月度销售趋势(万元)")

# 柱状:品类对比
by_category.plot(x="品类", y="销售额", kind="bar",
                 color="#EE854A", legend=False, ax=axs[1])
axs[1].set_title("品类销量对比(万元)")
axs[1].tick_params(axis="x", rotation=0)

# 饼:渠道占比
by_channel.plot(x="渠道", y="销售额", kind="pie",
                autopct="%1.0f%%", legend=False, ax=axs[2])
axs[2].set_title("渠道占比")
axs[2].set_ylabel("")

fig.suptitle("2026 上半年销售三图速览", fontsize=14)
plt.savefig("sales_dashboard.png", dpi=150, bbox_inches="tight")
plt.show()

注意这里出现了一个熟悉又新鲜的组合:by_month.plot(..., ax=axs[0])。ax= 参数就是传送带的接口——它说”别开新画布,画到我指定的格子里去”。于是第67篇的子图布局 + 第33篇的分组统计 + 今天的 df.plot(),三层手艺拼成了一台出图流水线。

第 3 步:数据一变,图跟着变

流水线真正的威力在”重跑”。往 sales_detail.csv 里加一行七月的数据(或者直接换一份新 CSV),再跑一遍 pipeline_plot.py——三张图自动带上新数据,趋势线多一个点、柱子重新排队、饼图比例刷新。你一行代码都不用改,因为图不是”画”出来的,是”算出来”的。

想更进一步可以顺手做个”半自动”:把文件名提到开头 FILE = "sales_detail.csv",以后换数据只改这一行;或者像第57篇那样,在脚本结尾 print("图表已生成:sales_dashboard.png"),给自己一个明确的完成信号。

四、改进:跟AI一起升级

把 pipeline_plot.py 发给 AI,需求往”汇报级”上提:

“这是我的销售明细 CSV 和出图脚本。帮我升级成’月度汇报一键出图’:1)趋势图上标出最高月和最低月的数值;2)三个小图统一成’一强调多安静’的配色,销售额用强调色、其他用灰色系;3)品类柱状图按销售额从高到低排序;4)脚本开头加文件名参数,换 CSV 不用改代码。先说方案再给代码。”

它的方案里这几个点值得追问:

  • 让它解释 ax= 的画布流向:df.plot(ax=axs[0]) 和 axs[0].plot(...) 有什么区别?合格的回答会说:前者把 pandas 的列名魔法和 matplotlib 的画布管理接通,后者要你自己拆数据。看懂这个接口,你以后往任何格子里”喂表”都不求人;
  • 排序要追一句”数据口径”:让它按销售额降序时问自己——品类的顺序是按半年总和排,还是按 6 月单月排?口径不对,图讲的故事就错。让它把排序用的中间表 print 出来,你亲眼看一眼;
  • 追问”换数据会怎样”:让它模拟”下个月 CSV 多一列’利润'”的情景,脚本哪几行要改?好的方案是把”用哪些列”也变成文件开头的配置项,数据变了改一处即可。

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

  1. 月份顺序被排错,趋势图折返跑。 groupby("月份") 默认按拼音/字母排序,”10月”会排到”2月”前面,折线图直接画成心电图。疫苗:时间类的横轴要显式指定顺序——用 pd.Categorical 设 categories=["1月","2月",...],或者干脆在明细表里加一列 月份数字 专供排序。凡是带顺序的横轴(月份、季度、星期),都别信默认排序。
  1. kind="pie" 遇到多列 y 就画出重叠的圆。 饼图一次只能吃一列数据,你写 y=["销售额", "成本"] 它就把两个饼叠在一起,看着像抽象画。疫苗:饼图永远只传一列(y="销售额"),而且要 x= 指明标签列;顺手 axs[2].set_ylabel("") 清掉多余的纵轴标题,饼图没有”纵轴”。
  1. 中文列名在 Excel 打开的 CSV 里读出来是乱码。 Excel 存的 CSV 是 GBK 编码,pd.read_csv("文件.csv") 默认按 UTF-8 读,满屏 锟斤拷。疫苗:读之前先确认来源——自己存的加 encoding="utf-8-sig",Excel 存的用 encoding="gbk";报 UnicodeDecodeError 就是编码不对,换编码重读,别硬改数据。
  1. 图例、刻度、标题挤成一团,数据对了图却没法看。 df.plot() 默认给每列开图例、横轴刻度不旋转,品类名一多就叠成黑条。疫苗:画完必须亲眼看一下 PNG;柱状图加 ax.tick_params(axis="x", rotation=0)(或旋转 45 度),单条线用 legend=False,多条线用 legend=True 并配合 constrained_layout=True。一句话:df.plot() 负责”画出来”,排版手艺还是第66篇的活儿。

六、跟AI协作的正确姿势(数据+图表流水线特供版)

  1. 给 AI 的不只是图,还有数据口径。 发需求时把”每张图的数据从哪来”写清楚:”趋势=按月份 sum,对比=按品类 sum,占比=按渠道 sum”。不说口径,AI 只能猜你 groupby 的是哪列;猜错的图看起来照样漂亮,但数字是别人的数字。
  1. 让它把流水线拆成”读、算、画”三段。 要求它把代码组织成三个清晰的段落(或三个函数),你以后换数据只动”读”那段,改口径只动”算”那段,改样式只动”画”段。一锅粥的脚本第一次跑完就再也改不动了——可改性比一次跑通更重要。
  1. 让它当”数据体检员”。 出图之前先要一段校验代码:有没有空值?销售额有没有负数?月份是不是 12 个?让它加在流水线开头,数据不对就 print 警告。图表画错九成不是 matplotlib 的锅,是数据先脏了——先验数据,再画图,这个顺序永远别反。

七、今日小练习

练习1:三图速览。 找一份你自己的 CSV(或用文中的 sales_detail.csv),用 groupby 算出三组汇总,把折线、柱状、饼图排成 1×3 一页导出 PNG(提示:by_month = df.groupby("月份", as_index=False)["销售额"].sum();每张图用 df.plot(x=..., y=..., kind=..., ax=axs[0]) 指定画到哪个格子;fig, axs = plt.subplots(1, 3, figsize=(13, 4.5)))。

练习2:数据一变图就变。 在练习1的 CSV 里加一行新月份的数据,原封不动重跑脚本,确认三张图都更新了;再故意把某个月的销售额改成负数,看图会画成什么样(提示:流水线不用改一行代码,这就是它的价值;负数会让纵轴下探,趋势图出现”深坑”——想想真实业务里什么情况会出现负数,退货?)。

练习3:修好”折返跑”的月份。 故意让 groupby("月份") 默认排序出一张”1月、10月、11月、12月、2月……”的折线图,然后用 pd.Categorical 修好它(提示:df["月份"] = pd.Categorical(df["月份"], categories=["1月","2月","3月","4月","5月","6月"], ordered=True),再 groupby;对比修复前后的两张图,体会”顺序也是数据的一部分”)。

下篇预告

下一篇(第69篇):交互图表入门 — plotly基础,让图表”能动”。 今天出的图是静态 PNG,老板想看某个点的具体数值,只能凑近了瞪眼猜。下一篇引入 plotly:鼠标悬停显示数值、滚轮缩放、点击筛选——图表从”照片”升级成”遥控器”。只学一个新概念:plotly 的交互式图表,matplotlib 的手艺照旧能用上。