第 4.3 节 常见问题#
这一节收集了初学者在「做汇总」环节最常卡的 10 个问题。每个问题都给出直接答案,能跑代码的都附上完整示例。
怎么用这一节 不用通读。做报表时遇到问题用 Ctrl+F 搜关键词,直接跳到对应那条。
准备一下数据,下面每题都会用到:
import pandas as pd
import numpy as np
data = {
"订单号": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
"下单日期": ["2024-01-05", "2024-01-08", "2024-01-12", "2024-02-03",
"2024-02-15", "2024-03-02", "2024-03-20", "2024-04-08"],
"城市": ["北京", "上海", "北京", "广州", "上海", "北京", "深圳", "广州"],
"商品": ["手机", "电脑", "耳机", "手机", "键盘", "显示器", "耳机", "手机"],
"销量": [2, 1, 3, 2, 5, 1, 4, 2],
"单价": [3999.0, 7999.0, 599.0, 3999.0, 299.0, 1299.0, 599.0, 3999.0],
"金额": [7998.0, 7999.0, 1797.0, 7998.0, 1495.0, 1299.0, 2396.0, 7998.0],
"客户评分": [4.5, 4.0, 4.8, 3.9, 4.2, 4.6, 4.1, 4.9],
}
df = pd.DataFrame(data)Q1:groupby() 的基本工作原理是什么?#
答: 三步:拆 → 算 → 拼。
result = df.groupby("城市")["金额"].sum()- 拆——按「城市」把 8 行拆成 4 组(北京 3 行、上海 2 行、广州 2 行、深圳 1 行);
- 算——每一组单独把「金额」加起来;
- 拼——把 4 个和拼成一张表。
print(result)城市
上海 9494.0
北京 11094.0
广州 15996.0
深圳 2396.0
Name: 金额, dtype: float64结果有两个特点,记住了就不会出错:
- 返回的是 Series(因为你只选了一列去聚合);
- 分组键变成了索引,不再是普通列。
第 2 点带来的实际影响:groupby 的结果想存成 CSV,第一列会是空的。存之前记得
reset_index()。df.groupby("城市")["金额"].sum().reset_index().to_csv("result.csv", index=False)
Q2:如何统计每个分组的记录数量?#
答: 用 size()。
print(df.groupby("城市").size())城市
上海 2
北京 3
广州 2
深圳 1
dtype: int64另一种写法:value_counts()。
print(df["城市"].value_counts())北京 3
上海 2
广州 2
深圳 1
dtype: int64两者的区别:
groupby().size() | value_counts() | |
|---|---|---|
| 结果类型 | Series(索引是分组键) | Series(索引也是分组键) |
| 排序 | 按分组键排序 | 按数量从多到少排 |
| 能否对多列分组 | 能(groupby(["A","B"]).size()) | 不能 |
| 结果能不能直接当表用 | 能,reset_index() 即可 | 不能,它是 Series |
要数「笔数」就用
size(),不要用count()。count()会被缺失值影响(见下一题),size()永远等于组里的行数。
Q3:count() 和 size() 有什么区别?#
答: count() 数的是「非空值的个数」,size() 数的是「行数」。
看例子(假设「金额」有一个缺失值):
d = df.copy()
d.loc[1, "金额"] = np.nan
print(d.groupby("城市")["金额"].count())城市
上海 1
北京 3
广州 2
深圳 1
dtype: int64print(d.groupby("城市").size())城市
上海 2
北京 3
广州 2
深圳 1
dtype: int64上海的订单号是 1002(金额缺失)和 1005(金额 1495)。count() 数非空值 → 1;size() 数行数 → 2。
选型速查
- 「这个城市有多少笔订单」→
size()- 「这个城市有多少客户填了金额」→
count()- 「这个城市一共有多少条记录」→
size()一句话:只要业务上的「条数」,就用
size(),它不会被缺失值带偏。
Q4:如何对同一列同时计算多个统计指标?#
答: 给 agg() 传一个列表。
print(df.groupby("城市")["金额"].agg(["count", "mean", "sum", "max", "min"])) count mean sum max min
城市
上海 2 4747.0 9494.0 7999.0 1495.0
北京 3 3698.0 11094.0 7998.0 1299.0
广州 2 7998.0 15996.0 7998.0 7998.0
深圳 1 2396.0 2396.0 2396.0 2396.0几个常用统计量:
df.groupby("城市")["金额"].agg(["sum", "mean", "std", "min", "max"])std 是标准差,反映这一组数据的离散程度。
想要更漂亮的列名,用重命名:
g = df.groupby("城市")["金额"].agg(["sum", "mean"])
g.columns = ["总金额", "平均金额"]
print(g) 总金额 平均金额
城市
上海 9494.0 4747.0
北京 11094.0 3698.0
广州 15996.0 7998.0
深圳 2396.0 2396.0Q5:如何使用 agg() 为不同列指定不同的聚合函数?#
答: 传字典,键是列名,值是聚合函数。
print(df.groupby("城市").agg({"金额": "sum", "销量": "mean"})) 金额 销量
城市
上海 9494.0 3.0
北京 11094.0 2.0
广州 15996.0 2.0
深圳 2396.0 4.0同一列要多个统计量,值写成元组列表:
print(df.groupby("城市").agg({"金额": ["sum", "mean"], "销量": "sum"})) 金额 销量
sum mean sum
城市
上海 9494.0 4747.0 6
北京 11094.0 3698.0 6
广州 15996.0 7998.0 4
深圳 2396.0 2396.0 4命名聚合(推荐,列名更清晰):
print(df.groupby("城市").agg(总金额=("金额", "sum"), 订单数=("订单号", "count"))) 总金额 订单数
城市
上海 9494.0 2
北京 11094.0 3
广州 15996.0 2
深圳 2396.0 1命名聚合的值必须是元组:
agg(新名=("原列名", "函数"))。 写成agg(总金额="sum")会报TypeError: Must provide 'func' or tuples of '(column, aggfunc)'—— 那个语法是给DataFrame.agg()用的,在groupby上不管用。
Q6:transform() 和 agg() 有什么区别?#
答: 核心区别是结果的长度。
agg():每组算出一个值 → 结果变短(4 个城市 4 行);transform():每组算出一个值,再广播回组里的每一行 → 结果长度不变(8 行)。
print(df.groupby("城市")["金额"].agg("sum"))城市
上海 9494.0
北京 11094.0
广州 15996.0
深圳 2396.0print(df.groupby("城市")["金额"].transform("sum"))0 11094.0
1 9494.0
2 11094.0
3 15996.0
4 9494.0
5 11094.0
6 2396.0
7 15996.0transform() 最大的用途是「造新列」:
d = df.copy()
d["城市总金额"] = d.groupby("城市")["金额"].transform("sum")
d["占本城比例"] = d["金额"] / d["城市总金额"]
print(d[["城市", "金额", "占本城比例"]].round(3)) 城市 金额 占本城比例
0 北京 7998.0 0.721
1 上海 7999.0 0.843
2 北京 1797.0 0.162
3 广州 7998.0 0.500
4 上海 1495.0 0.157
5 北京 1299.0 0.117
6 深圳 2396.0 1.000
7 广州 7998.0 0.500选型口诀
- 要汇总成一张小表 →
agg()- 要在原表上加一列组级别的信息 →
transform()常见报错:想把
agg("sum")的结果直接和原表相除,结果长度对不上(4 行 vs 8 行)。 这时候要么先reset_index()合并回去,要么改用transform()。
Q7:pivot_table() 与 groupby() 有什么区别?#
答: 三个区别。
groupby() | pivot_table() | |
|---|---|---|
| 维度 | 一般是一维(一个分组键) | 二维(行 + 列两个分组键) |
| 默认聚合 | 你指定(sum、mean…) | mean(平均) |
| 单元格没数据时 | 不存在这个位置 | 填 NaN |
一维汇总用 groupby:
df.groupby("城市")["金额"].sum()二维交叉用 pivot_table:
pd.pivot_table(df, index="城市", columns="商品", values="金额", aggfunc="sum")商品 手机 显示器 电脑 耳机 键盘
城市
上海 NaN NaN 7999.0 NaN 1495.0
北京 7998.0 1299.0 NaN 1797.0 NaN
广州 15996.0 NaN NaN NaN NaN
深圳 NaN NaN NaN 2396.0 NaN
pivot_table的aggfunc默认是「平均」不是「求和」!pd.pivot_table(df, index="城市", values="金额") # 平均值 pd.pivot_table(df, index="城市", values="金额", aggfunc="sum") # 求和这是透视表最常见的坑,永远显式写
aggfunc。
Q8:pivot_table() 中的 index、columns、values 和 aggfunc 分别有什么作用?#
答: 一句话记住——行、列、值、算法。
pd.pivot_table(
df, # 数据源
index="城市", # 行:按谁分行
columns="商品", # 列:按谁分列
values="金额", # 值:算什么
aggfunc="sum" # 怎么算
)| 参数 | 作用 | 类比 Excel |
|---|---|---|
index | 行方向的分组键 | 拖到「行标签」 |
columns | 列方向的分组键 | 拖到「列标签」 |
values | 要被聚合的列 | 拖到「值」 |
aggfunc | 聚合方式,默认 mean | 「值」的汇总方式设置 |
两个实用的补充参数:
# 空格子填 0(默认是 NaN)
pd.pivot_table(df, index="城市", columns="商品", values="金额",
aggfunc="sum", fill_value=0)
# 加合计行/列
pd.pivot_table(df, index="城市", values="金额", aggfunc="sum",
margins=True, margins_name="合计") 金额
城市
上海 9494.0
北京 11094.0
广州 15996.0
深圳 2396.0
合计 38980.0Q9:crosstab() 如何统计两个或多个分类变量的交叉频数?#
答: crosstab() 专门数「组合出现了多少次」。
两个变量:
print(pd.crosstab(df["城市"], df["商品"]))商品 手机 显示器 电脑 耳机 键盘
城市
上海 0 0 1 0 1
北京 1 1 0 1 0
广州 2 0 0 0 0
深圳 0 0 0 1 0三个变量(前两个做交叉表,第三个用 values= 指定):
print(pd.crosstab([df["城市"], df["商品"]], df["销量"]))销量 1 2 3 4 5
城市 商品
上海 电脑 1 0 0 0 0
键盘 0 0 0 0 1
北京 手机 0 1 0 0 0
显示器 1 0 0 0 0
耳机 0 0 1 0 0
广州 手机 0 2 0 0 0
深圳 耳机 0 0 0 1 0算比例:
print(pd.crosstab(df["城市"], df["商品"], normalize="index").round(2))商品 手机 显示器 电脑 耳机 键盘
城市
上海 0.00 0.00 0.50 0.00 0.50
北京 0.33 0.33 0.00 0.33 0.00
广州 1.00 0.00 0.00 0.00 0.00
深圳 0.00 0.00 0.00 1.00 0.00normalize 的三个取值:
| 取值 | 含义 |
|---|---|
"index" | 按行归一化,每行加起来 = 1 |
"columns" | 按列归一化,每列加起来 = 1 |
"all" | 按总计归一化,整张表加起来 = 1 |
crosstab和pivot_table怎么选?
- 只想要「有多少笔」→
crosstab()- 想要「每格是多少钱、卖了多少件」→
pivot_table()它们的
normalize参数用起来很像,都支持"index"/"columns"/"all"。
Q10:如何按照多个字段分组,并处理分组结果中的缺失值?#
答: 用列表传多个分组键。
g = df.groupby(["城市", "商品"])["金额"].agg(["count", "sum"])
print(g) count sum
城市 商品
上海 电脑 1 7999.0
键盘 1 1495.0
北京 手机 1 7998.0
显示器 1 1299.0
耳机 1 1797.0
广州 手机 2 15996.0
深圳 耳机 1 2396.0结果索引是两层的,可以这样取用:
g.loc["北京"] # 北京这一组
g.loc["北京", "手机"] # 北京-手机这一组
g.loc[("北京", "手机"), "sum"] # 只要总金额这一列
g.index.levels[0] # 第 0 层:城市列表处理缺失值:
先看分组统计里的 NaN 是怎么来的:
d = df.copy()
d.loc[1, "金额"] = np.nan
print(d.groupby("城市")["金额"].mean())城市
上海 1495.0
北京 3698.0
广州 7998.0
深圳 2396.0
dtype: float64上海的金额本来是 7999.0 和 1495.0,现在一个是 NaN。mean() 默认跳过缺失值(skipna=True),只用 1495.0 来算,所以结果是 1495.0。
三种应对方式:
# 方式 1:把缺失当 0(适合「没卖就是 0」的业务)
d["金额"] = d["金额"].fillna(0)
d.groupby("城市")["金额"].mean()
# 方式 2:让缺失被跳过(默认行为,适合「没填就是没数据」)
d.groupby("城市")["金额"].mean()
# 方式 3:先删掉有缺失的行再分组
d.dropna(subset=["金额"]).groupby("城市")["金额"].mean()选择依据是业务含义,不是技术偏好。 「这单没填金额」到底代表卖了但没记金额,还是这单不算钱?
- 前者 → 方式 2(跳过)或方式 3(删掉);
- 后者 → 方式 1(当 0)。
另外记住:
mean()的分母会随缺失值变小。 如果你需要「有效样本的平均」,那正是mean()的行为;如果你需要「按全部订单数摊薄」,得先fillna(0)。
小结#
这一节的 10 个问题,归结起来是四组问题:
- 分组怎么用——
groupby()拆算拼;结果里分组键变成索引;size()数条数,count()数非空; - 统计怎么做——
agg([...])同列多指标,agg({...})不同列不同算法,命名聚合更清晰; agg还是transform——变短用agg,保持行数用transform;- 二维报表怎么做——
pivot_table()出数值报表(aggfunc默认 mean),crosstab()出频数表(支持normalize)。
下一章预告 第 5 章讲合并与重塑:
merge()把订单表和客户表「横向拼」起来,concat()把多张表「竖着摞」,melt()把「一季度、二季度、三季度」这三列变成三行,apply()让你写自己的变换逻辑。