资讯动态

Pandas合并与分组聚合:从原理到实战

发布时间:2026/10/21 22:43:29 来源:尧图企业网站定制
处理多张表的数据时大部分时间其实不是花在“算”上而是花在“拼”和“分”上。这里的“拼”指的是把分散在不同表格里的数据合并到一起“分”则是按某个维度把数据拆开再聚合。Pandas 里对应的就是 DataFrame 的合并concat / merge / join和分组聚合groupby。这是数据分析流程里绕不开的一环只要做过一次稍微像样的数据分析就一定会用到这两类操作。这篇内容从实际场景出发把合并和分组聚合的常用写法、背后的逻辑、以及踩过的坑都过一遍适合刚入门 Pandas 但想把这些操作真正用熟练的同学。我自己的体会是很多教程分开讲 concat、merge、groupby 的时候都很清楚但一到真实数据上就容易乱。原因在于真实场景里数据不会乖乖等你索引会对不上、列名会重复、分组维度会嵌套、聚合完之后索引还乱糟糟。所以这篇会刻意按“为什么要这么写”的思路来讲配合可直接跑通的代码示例尽量让每个操作都能落到实际使用场景里去理解。1. 内容整体设计与思路拆解1.1 为什么合并和分组聚合总是成对出现数据从采集到能用来分析中间经历的清洗步骤各不一样但有两件事几乎是固定的需要把多个来源的数据拼接在一起然后按业务维度做汇总统计。举个例子电商场景下订单表、用户表、商品表通常是分开存储的。订单表里有 user_id 和 product_id但如果你想知道“哪个城市的用户贡献了最多的销售额”就得先把订单表和用户表关联起来拿到城市字段再按城市做聚合。这就是合并和分组聚合配合使用的最典型场景。合并解决的是“数据在哪里”的问题——把散落的信息拼成一张完整的大宽表。分组聚合解决的是“数据意味着什么”的问题——从具体记录里提炼出规律和结论。分析流程中这两个环节缺一不可而且顺序通常也是固定的先合并后聚合。1.2 先把三个核心概念理清楚Pandas 的合并操作有三个常用工具concat、merge、join。很多同学容易搞混其实它们各有侧重。concat 是“拼接”无论是横向还是纵向它默认不关心列名是否匹配只负责把多份数据按轴方向堆在一起。merge 是“关联”类似 SQL 里的 join它通过一个或多个 key 列把两份数据匹配起来这才是真正意义上的“合并”。join 其实是 merge 按索引关联的便捷版本如果你的关联条件是索引而不是普通列用 join 写起来会省事很多。分组聚合的核心则是 groupby它做的事情可以拆成两步先按某个或某几个列把数据切成若干组然后对每一组应用聚合函数求和、均值、计数等最后再把结果汇总返回。理解这几个概念的区别后面写代码就不会出现“该用 merge 的时候用了 concat结果列全成了 NaN”这种问题。2. 数据合并实操把多张表拼成一张表2.1 concat 的两种方向与容易踩的坑concat 最简单的用法是纵向拼接多份结构相同的 DataFrame比如按月份拆分的销售记录每个月一张表字段完全一致月底要合并成全年数据这种场景直接用 concat 纵向拼接就对了。import pandas as pd jan pd.DataFrame({user_id: [1, 2, 3], amount: [100, 200, 150]}) feb pd.DataFrame({user_id: [4, 5, 6], amount: [300, 250, 180]}) result pd.concat([jan, feb], axis0, ignore_indexTrue) print(result)axis0 表示沿着行方向堆叠也就是把第二个 DataFrame 的行追加到第一个下面。ignore_indexTrue 非常关键它会让合并后的结果重新生成 0 到 n-1 的索引否则你会看到索引是 0、1、2、0、1、2 这样的重复状态后面再做筛选或去重时容易出现莫名其妙的 bug。concat 也可以做横向拼接也就是 axis1。这时候它把两个 DataFrame 按列并排放置相当于把两张表的列拼成一张更宽的表。横向拼接时有一个默认行为很多人不知道它是按索引对齐的而不是按位置对齐。left pd.DataFrame({key: [A, B, C], val: [1, 2, 3]}) right pd.DataFrame({key: [B, C, D], val2: [4, 5, 6]}) wide pd.concat([left, right], axis1) print(wide)这段代码运行后left 和 right 的索引都是 0、1、2但它俩对应的 key 并不相同。concat 不管这些它只按索引位置把列拼在一起于是会出现左边 key 是 A右边 key 是 B 这种错位的宽表。这就是滥用 concat 横向拼接的典型问题如果你要做的是“按 key 关联”应该用 merge而不是 concat。另外 concat 还有一个 join 参数可以控制拼接时如何处理索引对齐默认是 outer也就是保留所有索引改成 inner 则只保留交集。提示concat 适合结构相同的数据直接堆叠比如按月、按日拆分的文件合并。涉及按某个 key 关联两张表时优先考虑 merge。2.2 merge真正的关联操作merge 的基本用法是指定两个 DataFrame 和一个共同的关联列然后选择关联方式。关联方式通过 how 参数控制有四种inner、left、right、outer。orders pd.DataFrame({ order_id: [101, 102, 103], user_id: [1, 2, 1], amount: [50, 300, 80] }) users pd.DataFrame({ user_id: [1, 2, 3], city: [上海, 北京, 广州] }) merged pd.merge(orders, users, onuser_id, howleft) print(merged)这段代码用 user_id 作为关联键把用户表中的城市信息补充到订单表里。howleft 表示以左表orders为基准左表所有行都保留右表有匹配的才填进来没匹配到的城市就是 NaN。选择 how 参数是新手最容易纠结的地方我的建议是先想清楚“分析结果需要保留哪些行”。如果订单表是主表每条订单都必须存在用户信息只是补充那就用 left。如果只想要两边都能匹配上的记录比如只想分析有订单又有用户信息的记录用 inner。如果想知道哪些用户在订单表里完全没有数据可以用 right 或者 outer 再筛 NaN。merge 还有一个常用场景是关联列名不一致比如左表叫 user_id右表叫 id。这时候需要分别指定 left_on 和 right_onusers_rename pd.DataFrame({ id: [1, 2, 3], city: [上海, 北京, 广州] }) merged2 pd.merge(orders, users_rename, left_onuser_id, right_onid, howleft)关联成功后结果里会同时保留 user_id 和 id 两列如果你只需要其中一个可以用 drop 删掉多余的列。多键关联也是 merge 的常见用法比如订单表和商品快照表需要同时按 product_id 和 date 才能唯一匹配这时候 on 参数可以传一个列表pd.merge(df1, df2, on[product_id, date], howinner)多键关联在真实业务里非常常见尤其是事实表和维度表之间存在时间维度上的变化时单靠一个键往往会关联出重复行。提示merge 关联后如果发现行数比左表还多大概率是右表的关联键有重复值这是最常见的隐患后面会专门讲。2.3 join 与 merge 的取舍join 是 merge 的索引关联版本。当两个 DataFrame 的关联条件恰好就是索引时用 join 可以少写一点代码df_a pd.DataFrame({val: [1, 2, 3]}, index[x, y, z]) df_b pd.DataFrame({info: [10, 20, 30]}, index[y, z, w]) joined df_a.join(df_b, howleft) print(joined)这段代码按索引 x、y、z、w 做左连接df_a 的所有行保留df_b 中能匹配到的索引y、z补充进来w 在左表没有对应索引所以不会出现。其实 join 也可以用 on 参数指定普通列来关联但它背后仍然会把该列设为索引再合并遇到关联键是索引列且你不想动原表时join 写起来确实更直观。我的习惯是数据里有关联键列就用 merge关联键就是索引或者想把某个列当作索引来做连接就用 join。两种写法只是入口不同底层都是类似的哈希匹配逻辑。3. 分组聚合从明细数据到业务结论3.1 groupby 基础用法与分组后的结构变化groupby 的经典写法是df.groupby(列名)[目标列].聚合函数()。这一行代码的背后做了三件事按列分组、选择要聚合的列、执行聚合计算。sales pd.DataFrame({ city: [上海, 北京, 上海, 广州, 北京, 广州], product: [A, B, A, B, A, B], amount: [100, 200, 150, 300, 120, 250] }) city_sum sales.groupby(city)[amount].sum() print(city_sum)运行结果是一个 Series索引是城市名值是各城市的销售总额。注意这个结果其实是一个带索引的 Series方便后续按城市名索引取值但如果想把它作为普通 DataFrame 用就需要处理索引。groupby 支持一次按多个列分组传入一个列表即可这样会得到 MultiIndex。比如同时按城市和产品分组grouped sales.groupby([city, product])[amount].sum() print(grouped)这时分组维度是两级city 和 product取值时可以通过元组来定位比如 summary[上海, A] 就能拿到上海 A 产品的销售总额。分组聚合的结果往往让人很意外的一点是原本的 DataFrame 里那些非分组、非聚合的列不会出现在结果里。因为你只对 amount 做了求和其他列没有参与任何聚合计算Pandas 会直接丢弃或把结果设计成只保留目标列。3.2 多列分组时的 MultiIndex 处理技巧MultiIndex 是分组聚合绕不开的话题。当分组维度超过两个时结果默认是行索引的多级结构这在后续处理中会遇到各种问题。一个问题是怎么把 MultiIndex 转回普通列。最直接的方法是 reset_index。它把索引里所有的层级都还原成普通列。result sales.groupby([city, product])[amount].sum().reset_index() print(result)这样得到的结果是一个普通的 DataFrame包含 city、product、amount 三列。reset_index 的同时还能传入 name 参数给聚合结果列重命名比如 reset_index(nametotal_amount)这样更明确。另一个常见操作是对分组后的结果继续做筛选或排序。比如想找每个城市销售额最高的产品靠头脑算不如靠代码。先把分组结果 reset 成表再按城市分组做 rankresult sales.groupby([city, product])[amount].sum().reset_index(nametotal) result[rank] result.groupby(city)[total].rank(ascendingFalse) top result[result[rank] 1] print(top)这个组合逻辑在实际分析中非常常用先多维度聚合再在组内继续排序拿到每组排名最高的记录。理解 MultiIndex 的生成和还原是灵活运用 groupby 的前提。提示reset_index 出现之前很多人会用 as_indexFalse 直接在 groupby 时关掉索引形式这也是一种避免后续 reset 的写法。sales.groupby([city, product], as_indexFalse)[amount].sum()3.3 一次聚合多种统计量agg 的核心用法groupby 配合单个聚合函数很基础但分析场景里往往一次要算好几个指标总和、均值、最大值、最小值、计数。每个指标单独写一行 groupby 再合并太傻了agg 就是干这个的。agg 的用法有两种。第一种是对所有选定列应用相同的聚合函数列表result sales.groupby(city)[amount].agg([sum, mean, max, min, count]) print(result)这会生成一个 DataFrame行是城市列是 sum、mean、max、min、count每个城市一行汇总。第二种是对不同列应用不同的聚合函数传入一个字典orders pd.DataFrame({ city: [上海, 北京, 上海, 广州], amount: [100, 200, 150, 300], order_cnt: [5, 8, 6, 10] }) result orders.groupby(city).agg({ amount: [sum, mean], order_cnt: sum }) print(result)字典的键是要聚合的列名值是要应用的函数可以是字符串函数名也可以是自定义函数、lambda。结果会生成多层列索引列名分别是 amount、order_cnt 以及它们对应的聚合函数名。agg 最大的优势是把多种聚合逻辑集中在一句代码里避免多次遍历数据。数据量大的时候这种写法在性能上的差异非常明显。3.4 transform 与 apply聚合结果如何回到原行分组聚合的结果是每个组一行但实际分析中经常需要把聚合值“广播”回原表的每一行。比如计算每个城市销售额的占比需要先知道每个城市的总销售额然后把总销售额拼回原始订单明细里再算每一行占城市总额的比例。这里有两种思路。一种是通过 merge 把聚合结果关联回原表另一种是用 transform 直接生成与原表相同行数的结果sales[city_total] sales.groupby(city)[amount].transform(sum) sales[pct] sales[amount] / sales[city_total]transform 的返回值形状和原表一致它会自动把每组的聚合结果映射回组内每一行。这样就不需要先聚合再 merge代码简洁而且不容易出错。apply 则更自由它可以把整个子 DataFrame 传给你的自定义函数让你在组内做更复杂的操作。比如对每个城市的产品做排序后取前两条def top_two(group): return group.nlargest(2, amount) result sales.groupby(city, group_keysFalse).apply(top_two)理解 transform 和 apply 的区别transform 要求返回与原组相同长度的结果apply 则可以返回任意形状。实际使用中能用 transform 解决的不要用 apply因为 transform 通常更快。4. 常见问题与排查技巧实录4.1 合并后行数变多或变少这是一个几乎每个人都会遇到的问题。merge 之后行数莫名其妙增加了原因几乎都是关联键在右表中不唯一。比如订单表每个用户有多条订单用户表里 user_id 是唯一的这时按 user_id merge 没问题。但如果用户表本身有重复的 user_idmerge 时右表的每个重复值都会和左表行配对导致结果行数膨胀。排查方法很简单在合并前检查右表关联键的唯一性。duplicated_count users.duplicated(subset[user_id]).sum() print(f重复 user_id 数量: {duplicated_count})如果发现重复先决定是保留一条还是按业务逻辑再聚合一层再去 merge。行数变少则通常是使用了 inner join某些关联不到的行被过滤掉了检查过滤是否符合业务预期。4.2 groupby 结果的索引到底要不要处理这个问题在实战里经常引起困惑。很多人 groupby 出来直接赋值给一个新变量后面代码却报错或者结果显示不对原因多是他以为结果是普通 DataFrame但实际上是带索引的 Series 或带 MultiIndex 的 DataFrame。判断办法很简单打印结果的 index 看看。如果索引是 city、product 这种业务字段说明它们还不是普通列。要让它们变成普通列用 reset_index。这里顺便说一个实际场景既要保留明细行数又想要聚合结果最简单的就是 transform前面已经展示过。如果既要聚合结果又要用它去和其他表关联那就用 merge把 reset_index 后的结果作为一个普通表参与关联。注意groupby 后直接对结果列做赋值操作比如 result[new_col] ...如果结果本身就是 Series会直接报错。一定要先把结果转成 DataFrame 再操作。4.3 常见错误速查表错误现象可能原因解决办法merge 后行数翻倍右表关联键有重复先检查并处理重复值concat 后列错位使用了 axis1 但数据需要按 key 关联改用 mergegroupby 结果无法用列名取值结果是 Series 或 MultiIndex用 reset_index 或检查是否该用中括号想给 groupby 结果新增列但报错结果不是普通 DataFrame先 reset_index 再做列操作agg 后列名变成多层对多列用了聚合列表用字典重命名或用 reset_index 整理transform 结果长度不对对非分组列做了 transform检查分组键和目标列是否一致这些问题的共同根源在于没有准确判断中间结果的类型和索引状态。我的经验是每做一步关键操作就打印一下 shape 和 index用这两个信息能排查掉 90% 以上的奇怪问题。4.4 合并与分组联动的经典报错场景举个例子先 groupby 得出每个城市的销售额再想把城市名和另一个维度表的城市代码关联。如果你直接 merge 一个 Series 和一个 DataFrame可能连列都对不上。正确做法是先把聚合结果 reset_index确保 city 是列而不是索引然后再 mergeagg sales.groupby(city, as_indexFalse)[amount].sum() city_code pd.DataFrame({city: [上海, 北京], code: [SH, BJ]}) result pd.merge(agg, city_code, oncity, howleft)as_indexFalse 的写法在这类场景里非常省心。它让分组结果从一开始就把分组键保留为列而不是移到索引里后面再跟其他表 merge 时就不用额外 reset。我自己的习惯是只要分组之后还要参与关联或后续处理一律用 as_indexFalse省得多次 reset。如果分组维度是多个列也同样可以用 as_indexFalseMultiIndex 就不会出现所有分组键都直接成为结果集的普通列。5. 一个完整小案例订单数据的城市维度汇总把前面讲的内容串起来用一个小案例模拟真实分析流程。假设我们有订单明细表、用户基础表想分析每个城市的订单量和销售额并找出销售额最高的产品。数据构造如下orders pd.DataFrame({ order_id: [101, 102, 103, 104, 105], user_id: [1, 2, 1, 3, 2], product: [A, B, A, C, C], amount: [100, 200, 150, 300, 120] }) users pd.DataFrame({ user_id: [1, 2, 3], city: [上海, 北京, 广州] })第一步先关联订单和用户把城市带进订单明细df pd.merge(orders, users, onuser_id, howleft)第二步按城市汇总订单量和销售额city_stats df.groupby(city, as_indexFalse).agg( order_cnt(order_id, count), total_amount(amount, sum) )这里用了一种比较新的 agg 写法指定新列名、来源列、聚合函数名生成的结果列名直接符合预期不用之后改名。agg 的 语法在 Pandas 较新版本里可用如果版本老可以用字典或后面 reset 的方式代替。第三步找出销售额最高的城市和城市内最畅销的产品top_city city_stats.sort_values(total_amount, ascendingFalse).iloc[0] city_product df.groupby([city, product])[amount].sum().reset_index(nametotal) idx city_product.groupby(city)[total].idxmax() top_product_per_city city_product.loc[idx] print(top_city) print(top_product_per_city)利用 idxmax 找到每个城市总销售额最大的产品所在行索引再通过 loc 取行这是处理“组内最值所在行”的常用技巧。整个过程跑下来从两张原始表到最后业务结论每步的数据类型和索引状态都能清晰掌握。这个案例的代码在你自己数据分析练习中可以直接当模板用换成自己的字段名和数据就能跑起来。按这个顺序走一遍之后merge 和 groupby 的配合就不再是零散的知识点了而是完整可复用的分析路径。我把这种“合并—聚合—列还原—组内筛选”的流程跑了不知道多少遍后来基本养成一个习惯所有步骤里哪里用了 groupby哪里用了 merge都会先想清楚这一步骤的结果是 DataFrame 还是 Series、索引是业务列还是数字索引。判断清楚了后面几乎不会有突发性的报错。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑