“每个部门各卖了多少?””每人平均几单?””哪个区人均最高?”——这类分组对比问题,Excel的数据透视表要点七八下,还每次重来。
groupby一行分班、一行报数,透视表在你手里重生
一、老板的”第三轮追问”
前两篇的剧情走到这里:老板问”总销售额多少”(第31篇,总量类),又问”华东过万的订单有哪些”(第32篇,筛选类)。这次他把茶杯一放,问出了第三类问题:
“每个部门各卖了多少?做个对比。”
注意这个问题的形状:不再是”全表一个数”,也不再是”哪几行”,而是“按部门分成几堆,每堆各算一个数”。Excel里你点”插入→数据透视表→把’部门’拖到行、’销售额’拖到值”,七八下鼠标,出一张对比表。老板看完点头,然后说:”再加一列订单数,再加一列平均单值。“——你又回到拖拽界面,再拖两次字段。
这类问题叫分组统计,pandas的答案叫groupby。一行分班,一行报数:
print(df.groupby("部门")["销售额"].sum())
一行,出结果:
部门
华东 38300
华北 15300
华南 9800
Name: 销售额, dtype: int64
老板追加的”订单数、平均单值”?等你学完今天第二个小招agg,也是一行的事。今天的唯一一个新概念是:groupby——先把表按某一列”分班”,再对每班分别算数。它是Excel数据透视表的代码版,也是”读进程序→看清长相→按需提问”三部曲里最像”做报表”的一问。
二、新概念:groupby——给数据”分班”
先想清楚:它到底干了什么
groupby("部门")的动作,跟老师分班报数一模一样:
- 分班:看”部门”这一列,把华东的行放一堆、华南的行放一堆、华北的行放一堆;
- 报数:对每一堆,分别算你要的数(总和、平均、个数……)。
拿第32篇的示例数据(5行)走一遍:
姓名 部门 销售额 日期
0 小李 华南 8000 2026-01-03
1 小王 华东 12000 2026-01-05
2 小张 华东 15300 2026-01-07
3 小赵 华北 9500 2026-01-08
4 小王 华东 11000 2026-01-09
df.groupby("部门")先把班分好:华东班是1、2、4号行(12000、15300、11000),华南班是0号行(8000),华北班是3号行(9500)。接着["销售额"].sum()对每班求和:华东38300、华南8000……等等,上面输出华南是9800?对,因为那是一份更大的示例表,你手里的表跑出来是多少就是多少——报数结果取决于你的数据,别背我这里的数字。
读法记好:df.groupby("分班列")["要算的列"].算法(),念作”按某列分班,对某列报数“。
不只sum:报数算法全家福
.sum()只是报数员之一,同一行代码换个算法就是换个问题:
print(df.groupby("部门")["销售额"].sum()) # 各部门总销售额
print(df.groupby("部门")["销售额"].mean()) # 各部门平均单值
print(df.groupby("部门")["销售额"].count()) # 各部门订单数(数的是"有多少行")
print(df.groupby("部门")["销售额"].max()) # 各部门最大单子
sum(总和)、mean(平均)、count(计数)、max/min(最大/最小)——这五个覆盖日常报表九成需求。注意count数的是行数,也就是”订单数”,这正是老板要的第二列。
不指定列,整表分班报数
方括号里的列名其实可以省:
print(df.groupby("部门").sum(numeric_only=True))
销售额
部门
华东 38300
华北 15300
华南 9800
对所有数字列分别求和(numeric_only=True是”只算数字列”,不加的话日期列报错,坑3细说)。表有几列数字就出几列结果——销售额、利润、件数一次全算完。只想看一列就指定列,想全算就省略,按需选。
分班结果也是”表”,能接着排序
groupby吐出来的结果,索引是部门名、值是算出来的数——它也是个(小)表格,所以第32篇学的sort_values直接接着用:
各部门总额 = df.groupby("部门")["销售额"].sum()
print(各部门总额.sort_values(ascending=False))
老板要”对比”,一般就想要从高到低排好的对比。groupby出结果、sort_values排座次,两行一气呵成——这就是把学过的招连起来的甜头。
三、agg:一次报三个数
老板要三列:总销售额、订单数、平均单值。写三行groupby?可以,但笨——分了三遍班。agg(aggregate,”汇总”的缩写)的正确姿势是分一次班,报三个数:
报表 = df.groupby("部门")["销售额"].agg(["sum", "count", "mean"])
print(报表)
sum count mean
部门
华东 38300 3 12766.7
华北 15300 1 15300.0
华南 9800 1 9800.0
agg(["sum", "count", "mean"])就是给每班派了三个报数员:agg吃一个名单(第8篇的列表),名单里是算法名的字符串。列名现在是sum、count、mean,想换成中文表头(老板看的表必须是中文),加一行改名:
报表.columns = ["总销售额", "订单数", "平均单值"]
报表.columns是”表的列名清单”,直接赋一个新列表就整体换掉。到这里,老板的三列对比表四行代码成型,还顺手能sort_values排好序。
如果要对不同列算不同数(比如销售额求总和、订单数求计数),agg还接受字典写法:
报表 = df.groupby("部门").agg(总销售额=("销售额", "sum"), 订单数=("销售额", "count"))
print(报表)
字典里”新列名 = (老列名, 算法)”,一次出表连表头都起好了。两种写法记住一种就够用,另一种混个眼熟。
四、实战:老板要的”分部门对比表”
完整代码收拢,以后”分组对比”类问题就是换换列名的事:
import pandas as pd
df = pd.read_excel("销售明细.xlsx")
# 老板要的三列对比表
报表 = df.groupby("部门")["销售额"].agg(["sum", "count", "mean"])
报表.columns = ["总销售额", "订单数", "平均单值"]
报表 = 报表.sort_values("总销售额", ascending=False)
print("=== 分部门销售对比 ===")
print(报表)
# 追问1:每人呢?换个分班列而已
每人 = df.groupby("姓名")["销售额"].sum().sort_values(ascending=False)
print("n=== 每人销售排行 ===")
print(每人)
# 追问2:哪个区人均最高?(人均 = 总销售额 ÷ 订单数)
报表["人均单值"] = 报表["总销售额"] / 报表["订单数"]
print(f"n人均单值最高的部门:{报表['人均单值'].idxmax()},"
f"人均 {报表['人均单值'].max():.0f} 元")
输出(示例数据):
=== 分部门销售对比 ===
总销售额 订单数 平均单值
部门
华东 38300 3 12766.7
华北 15300 1 15300.0
华南 9800 1 9800.0
=== 每人销售排行 ===
姓名
小王 23000
小张 15300
小赵 9500
小李 8000
Name: 销售额, dtype: int64
人均单值最高的部门:华北,人均 15300 元
三个新小东西都在预料之内:换分班列=换老板的问题("部门"→"姓名");给表加新列就是报表["新列名"] = 两个旧列相除——跟给字典加键值对一个感觉(第8篇),表能一直”长”出新列;idxmax()是”最大值所在的那个索引名”(max()只给数,idxmax()给名字),找”冠军是谁”专用。
五、四个必踩的坑(先打疫苗)
坑1:groupby完直接print(df.groupby("部门")),打印出一坨看不懂的东西。 那是”分班对象”本身,不是结果——groupby只分完了班还没报数!铁律:groupby后面必须跟报数动作(.sum()、.count()、.agg(...)),跟上才算数。想看分班对象里到底有什么,用for 名字, 班级 in df.groupby("部门"):逐班打印,调试时偶尔用。
坑2:把count当”不重复个数”用。 count数的是行数(订单数)。如果一行是”一个客户”,客户买了三单就有三行,count报3——它可不知道这三行是同一个人。想数”有几个不同的客户”,用nunique()(number of unique,”不重复的有几个”):df.groupby("部门")["姓名"].nunique()报的是每部门有几个不同的人。报表数字不对劲时,先问自己:我要数的是”单数”还是”人数”?
坑3:整表sum时日期列捣乱。 df.groupby("部门").sum()如果表里有日期列,老版本pandas会把日期也加起来(一堆日期求和毫无意义)或者直接报错。加numeric_only=True(”只算数字”)就不会踩:df.groupby("部门").sum(numeric_only=True)。指定列的写法(["销售额"].sum())天然没这个坑——这也是”能指定列就指定列”的理由。
坑4:分班列里有空值(NaN),那一行”人间蒸发”。 groupby默认悄悄丢掉分班列为NaN的行,不报错、不吭声,你的人数/总额就这么少了。发报表前先查一眼:print(df["部门"].isna().sum())(这列有几个空),有空值就先处理——第29篇数据清洗的活儿:补上df["部门"] = df["部门"].fillna("未知"),让”未知”自成一班,报表才对得上总数。
六、跟AI协作的正确姿势(分组统计特供)
- 用”报表语言”描述需求。说”按部门分组,算总销售额、订单数、平均单值,列名用中文,按总销售额降序“——四要素(分班列、报数项、表头、排序)说全,AI写的
agg就不会缺项。它最常犯的错是忘numeric_only=True和把count/nunique搞混,拿到代码先查这两处 - 让AI”先出草表再加工”。追加一句:”先print一个只含sum的结果让我验数,再加另外两列“——分步出图。分组统计的错误非常隐蔽(数字都挺像样,就是不对),先拿最小的一列跟Excel手算对一下,再堆功能
- 追问”这个数怎么对不上”时,把两边数据都给AI。Excel的透视表结果+程序的输出一起贴给它,让它逐班核对。多数时候它能找出是坑2(单数vs人数)还是坑4(空值蒸发)——数字对不上先查这两处,八成就是它俩
高频句式:”表有这些列(贴df.dtypes的输出),按’某列’分组,对’某列’算sum/count/mean,输出中文表头并降序排列。请分步写代码,先出最小结果验数,注意numeric_only=True和空值处理“。
七、今日小练习
练习1:月度销售对比
按”日期”的月份分组,算每月总销售额和订单数,找出销售最好的月份。(提示:先把日期转成真日期df["日期"] = pd.to_datetime(df["日期"]),然后df.groupby(df["日期"].dt.month)["销售额"].agg(...)——注意分班列不一定非是”现成的列”,df["日期"].dt.month这个”算出来的列”也能当分班依据,这是groupby最实用的隐藏招。idxmax()找冠军月份。)
练习2:找出”抢单王”
按姓名分组,算每人订单数和平均单值,筛出”订单数最多但平均单值最低“的人。(提示:报表 = df.groupby("姓名")["销售额"].agg(["count", "mean"]),然后报表["count"].idxmax()找单量冠军、报表["mean"].idxmin()找平均垫底(idxmin是idxmax的孪生兄弟),两个名字是否同一人用if比一比——第5篇条件判断。)
练习3:一键报表生成器
写个程序:用户输入一个列名(比如”部门”或”姓名”),程序自动按该列分组、打印”总销售额、订单数、平均单值”三列中文报表并降序排列。列名写错要给提示不崩溃。(提示:用户输入是字符串,直接df.groupby(输入的列名)就行——但先用if 列名 in df.columns兜底(第31篇练习1的招),不在就print("没这列,看看拼写")。报数三件套和改列名是本文第三节的现成代码,整段搬进去。)
写在后面:从”手工拖拽”到”一行分班”
盘点今天:groupby是主菜——按列分班、逐班报数,df.groupby("分班列")["报数列"].算法()一句口诀;agg(["sum", "count", "mean"])一次报多个数,columns换中文表头;sort_values接着排座次,idxmax()/idxmin()找冠军。加上前两篇的读表、总量、筛选,”读进程序→看清长相→按需提问”的第三部曲已经齐了。
更值钱的还是思路:Excel的数据透视表是拖出来的,关掉重开又得拖;groupby是写下来的,改一列名重跑就出新表,存进脚本下周照样能跑。当”做报表”变成”改一行代码”,你和数据的关系就从”伺候它”变成了”使唤它”。
下一篇,数据开始”合体”了:两个表怎么拼成一个——”销售明细”和”员工档案”各记一半信息,merge/concat一行连起来。
下篇预告
第三十四篇:数据合并——merge与concat登场:两个表按”姓名”对上号拼成一张宽表,左右连接(left join)什么的不再是SQL黑话。老板说”把销售数据和员工档案合一起看”,一行代码交货。
发表回复