本节摘要:分组聚合(groupby)是 Pandas 分析能力的核心——"按门店统计销售额"这类需求一行代码搞定;合并连接(merge/concat)则解决"数据散在多张表"的问题。本节讲透 groupby 的分组-聚合-变换三段式,以及 merge 的四种连接方式。
阅读完本节,你应当能够:
"这家店这个月卖了多少""哪个类目贡献了最多收入"——这类问题每天都会问。如果靠手工筛选,每个门店写一遍代码,十个门店十段重复。groupby 把"按 XX 分组 → 对每组算 YY"封装成一个动作,一句代码完成全部组别的统计。
另一个高频场景是数据不在同一张表:订单表里有门店编号,门店信息(城市、负责人)在另一张表里。把它们拼起来,才能回答"哪个城市的订单最多"——这就是 merge 和 concat 的用武之地。
groupby 与 merge 解决两类不同的问题——"组内汇总"与"跨表拼接":
groupby 操作本质分三步:拆分(按分组键把行分成若干组)→ 应用(对每组执行聚合或变换)→ 合并(把结果拼回)。理解"拆-算-合"模型,就不会被眼花缭乱的 groupby 写法绕晕。
import pandas as pd df = pd.DataFrame({ "门店": ["北京", "上海", "北京", "广州", "北京"], "类目": ["数码", "数码", "家电", "家电", "服装"], "金额": [199, 355, 89, 1299, 450], }) # 按门店分组,算每组的销售额总和 df.groupby("门店")["金额"].sum() # 按门店+类目分组,算每组均值 df.groupby(["门店", "类目"])["金额"].mean()
agg 一次算多个统计量;transform 把组内统计广播回每一行(行数不变),常用于"计算每行相对组均值的偏差":
df.groupby("门店")["金额"].agg(["sum", "mean", "count"]) # transform:每行得到所在组的均值 df["组均值"] = df.groupby("门店")["金额"].transform("mean") df["偏差"] = df["金额"] - df["组均值"]
💡 关键直觉:
agg压缩行数(每组一行结果),transform保持行数(结果广播回原表)。想"分组后比较每行与组内水平"就用 transform。
merge 类似 SQL 的 JOIN,四种连接方式决定结果的取舍:
orders = pd.DataFrame({ "订单号": ["A001", "A002", "A003"], "门店编号": [1, 2, 1], "金额": [199, 355, 89], }) stores = pd.DataFrame({ "门店编号": [1, 2, 3], "城市": ["北京", "上海", "广州"], }) pd.merge(orders, stores, on="门店编号") # 内连接:只留两边都有的 pd.merge(orders, stores, on="门店编号", how="left") # 左连接:保留订单全部行 pd.merge(orders, stores, on="门店编号", how="right") # 右连接:保留门店全部行 pd.merge(orders, stores, on="门店编号", how="outer") # 外连接:全保留,缺的填 NaN
| how 参数 | 保留哪些行 | 典型用途 |
|---|---|---|
| inner(默认) | 键两边都有的 | 只分析能对上号的数据 |
| left | 左表全部 | 订单为主,补门店信息 |
| right | 右表全部 | 门店为主,看哪些无订单 |
| outer | 全部,缺的填 NaN | 盘点全量对账 |
jan = pd.DataFrame({"门店": ["北京"], "销售额": [120]}) feb = pd.DataFrame({"门店": ["上海"], "销售额": [150]}) pd.concat([jan, feb], ignore_index=True) # 上下拼接(行方向) pd.concat([df1, df2], axis=1) # 左右拼接(列方向)
# 按门店+日期分组汇总 summary = df.groupby(["门店", "日期"])["金额"].sum().reset_index() # 透视:行=门店,列=类目,值=金额和 pivot = df.pivot_table(index="门店", columns="类目", values="金额", aggfunc="sum")
reset_index() 把分组键从索引变回普通列,是分组后接续操作的标准动作。pivot_table 则是"分组聚合的表格化展示",一眼看出交叉汇总。
merged = pd.merge(orders, stores, on="门店编号", how="left") print(merged.isnull().sum()) # 检查左连接后有没有对不上的门店
⚠️ 常见坑:merge 后出现大量 NaN,多半是键值对不上(门店编号 3 没有订单)。先
isnull().sum()查缺失,再回源表看键值差异,别急着填数。
把订单表和门店表合并,回答"各城市的订单总额":
import pandas as pd orders = pd.DataFrame({ "订单号": ["A001", "A002", "A003", "A004"], "门店编号": [1, 2, 1, 3], "金额": [199, 355, 89, 1299], }) stores = pd.DataFrame({ "门店编号": [1, 2, 3], "城市": ["北京", "上海", "广州"], "负责人": ["王", "李", "张"], }) merged = pd.merge(orders, stores, on="门店编号", how="left") result = merged.groupby("城市")["金额"].sum().sort_values(ascending=False) print(result)
输出就是各城市销售额从高到低的排名——一条完整链路:合并补信息 → 分组聚合 → 排序出结论。
category 类型可显著加速 groupbygroupby 的返回结果,索引与列的形态有两种:
# 形态一:分组键留在索引里 s = df.groupby("门店")["金额"].sum() # 门店 是索引,金额 是值 # 形态二:分组键还原成普通列 df2 = s.reset_index() # 门店、金额 都是列
💡 关键直觉:聚合结果默认把分组键放在索引上。要继续 merge 或参与后续表格操作,先
reset_index()把键还原成列——这是分组后最常见的第一步。
| 聚合函数 | 用途 | 注意 |
|---|---|---|
sum |
总和 | 默认跳过 NaN |
mean |
均值 | 受异常值影响 |
median |
中位数 | 抗异常值 |
count |
非空计数 | 不含 NaN |
nunique |
唯一值数 | 常用于"有多少客户" |
first / last |
首末值 | 保留上下文 |
agg(["sum", "mean"]) |
多统计量 | 一次算多个 |
"每个门店有多少个不同客户"这类需求,用 nunique 一句话搞定:df.groupby("门店")["客户号"].nunique()。
# 每个门店内按金额排名(1 为最高) df["门店内排名"] = df.groupby("门店")["金额"].rank(ascending=False) # 每个门店销售额占比 df["门店占比"] = df.groupby("门店")["金额"].transform( lambda x: x / x.sum() )
组内排名、组内占比是业务分析高频操作,都建立在 groupby 的"拆-算-合"模型上——想清楚"按什么拆、每组算什么、结果放哪",一切分组操作都不难。
| 现象 | 原因 | 排查方法 |
|---|---|---|
| 合并后行数暴增 | 键有重复,笛卡尔积 | 先 drop_duplicates(subset=[键]) |
| 合并后大量 NaN | 键值对不上 | isnull().sum() + 看两表唯一值 |
| 键列名不同 | on 指定错误 | left_on / right_on 分别指定 |
| 类型不一致 | int 对 object | astype 统一后再 merge |
⚠️ 常见坑:merge 前不查重复键,是"合并后数据莫名其妙翻倍"的头号原因。两个表里都有重复的门店编号时,merge 会把它们两两配对——先对键去重,再合并。
groupby(键)[列].agg() 是标准句式pivot_table 把分组结果变成交叉表,一目了然isnull().sum() 查对不上的键,别急着填Pandas 的核心能力到这就齐了。下一章我们把这些技能系统化,专门对付"脏数据"——数据清洗与预处理。