返回首页

Roadmap
更新于 2026年10月24日 全文 1469 字未阅读

第 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()
  1. 拆——按「城市」把 8 行拆成 4 组(北京 3 行、上海 2 行、广州 2 行、深圳 1 行);
  2. 算——每一组单独把「金额」加起来;
  3. 拼——把 4 个和拼成一张表。
print(result)
城市
上海     9494.0
北京    11094.0
广州    15996.0
深圳     2396.0
Name: 金额, dtype: float64

结果有两个特点,记住了就不会出错:

  1. 返回的是 Series(因为你只选了一列去聚合);
  2. 分组键变成了索引,不再是普通列。

第 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: int64
print(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.0

Q5:如何使用 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.0
print(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.0

transform() 最大的用途是「造新列」:

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.0

Q9: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.00

normalize 的三个取值:

取值含义
"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 个问题,归结起来是四组问题:

  1. 分组怎么用——groupby() 拆算拼;结果里分组键变成索引;size() 数条数,count() 数非空;
  2. 统计怎么做——agg([...]) 同列多指标,agg({...}) 不同列不同算法,命名聚合更清晰;
  3. agg 还是 transform——变短用 agg,保持行数用 transform;
  4. 二维报表怎么做——pivot_table() 出数值报表(aggfunc 默认 mean),crosstab() 出频数表(支持 normalize)。

下一章预告 第 5 章讲合并与重塑:merge() 把订单表和客户表「横向拼」起来,concat() 把多张表「竖着摞」,melt() 把「一季度、二季度、三季度」这三列变成三行,apply() 让你写自己的变换逻辑。

阅读 --