☰
Pandas数据合并:concat与merge的核心区别与实战选型指南
2026/9/30 7:37:19 网站建设 项目流程

如果你写数据分析写了几年,大概率和我一样,对pd.concat和pd.merge又爱又恨。这两个 Pandas 合并 API 几乎是所有数据处理脚本的常客,但真正被问起"它俩到底有什么本质区别"时,很多人又说不清楚。更麻烦的是,一旦遇到多表关联、索引对齐、重复数据这类稍微复杂一点的场景,光靠背参数根本扛不住。

这篇文章不打算做函数文档的搬运工。我想把 Pandas 合并 API 背后那套设计逻辑拆开来讲——为什么 concat 和 merge 长得像、脾气却完全不同;什么样的场景该选谁;高阶用法里那些参数到底在解决什么问题。适合已经会用pd.concat(df1, df2)和pd.merge(df1, df2, on='key')跑通基础流程、但想真正理解合并机制、想写出更稳健代码的人。

1. 合并操作的两条底层路线:concat 是"物理拼接",merge 是"关系对齐"

1.1 先搞清楚 pd.concat 到底在做什么

我一直喜欢把pd.concat类比成"叠卡片"或者"拼积木"。它做的事情非常直接:沿着某个轴,把多个 DataFrame 或 Series 在物理上接在一起。默认axis=0是纵向堆叠,也就是把行接起来,像把两沓卡片上下叠在一起;axis=1是横向拼接,像把两张卡片左右并用。

关键在于:concat 不关心你的数据之间有没有"关系"。它不匹配行,不看索引,不看关键字段,它只是按照你指定的轴,把数据"堆放"起来。两个 DataFrame 列名不完全一致时,默认取并集,缺失的地方填 NaN;如果设置join='inner',则只保留两边都有的列,直接丢弃差异列。

举个例子,一段最常见的纵向拼接代码:

import pandas as pd df_jan = pd.DataFrame({ "user_id": [1, 2, 3], "amount": [100, 250, 180] }) df_feb = pd.DataFrame({ "user_id": [4, 5], "amount": [300, 120] }) df_all = pd.concat([df_jan, df_feb], ignore_index=True) print(df_all)

输出的结果就是 5 行数据,像把一月份账单和二月份账单直接摞在一起。这里没有匹配、没有筛选、没有对齐的语义,只有"再加几行"。

这个特性决定了它的适用边界:当你的多个数据块描述的是同一个"实体"、结构高度同构、且彼此之间不需要按字段匹配时,concat 是天然的选择。最常见的例子就是按月分表、按城市分片的日志数据,或者循环读取多个 CSV 后把结果堆到一起。

1.2 pd.merge 的本质是关系代数里的 Join

pd.merge的底层思路是完全另一套东西。它不是"拼积木",而是"对账"。它借鉴了关系数据库中的 Join 操作:通过一个或多个关键字段,把两张表里的行按照某种匹配规则组合起来。这个过程像拿着学生名单和成绩单,按学号把每个人的成绩填到名单上去。

merge 的每个参数都对应着关系代数的一个维度。how参数控制连接类型——内连接、左连接、右连接、全外连接、交叉连接;on指定两侧共用的连接键;如果连接键在两侧列名不同,就得用left_on和right_on分别指定。内在逻辑是:先确定"哪些行应该被匹配到一起",再根据连接类型决定哪些行保留下来。

代码层面的典型操作如下:

df_users = pd.DataFrame({ "user_id": [1, 2, 3, 4], "name": ["Alice", "Bob", "Charlie", "David"] }) df_orders = pd.DataFrame({ "user_id": [2, 3, 3, 5], "order_amount": [500, 700, 320, 980] }) df_merged = pd.merge(df_users, df_orders, on="user_id", how="left") print(df_merged)

这里的结果取决于how="left"的语义:以左表df_users的每一行为基准,去右表找匹配项。如果user_id在右表中出现多次,左表的行就会被复制展开;如果在右表中找不到,则对应字段填 NaN。

清楚了这一点,你就能理解很多"诡异"现象的来源——比如 merge 之后行数暴增,十有八九是连接键在某一侧或两侧存在重复值,触发了"一对多"甚至"多对多"的组合。这不是 Pandas 的 bug,而是关系代数本身的规则。

1.3 两条路线为什么不能混着用

把 concat 当 merge 用、或者把 merge 当 concat 用,是新手最容易踩的坑,而且踩了往往还一脸懵。

先说用 concat 模拟横向"关联"。假设我有两张表,一张存用户基本信息,一张存消费记录,我想按行把它们的列拼起来。有人会说,那用pd.concat([df1, df2], axis=1)不就行了吗?

行倒是行,但风险非常大。concat(axis=1)是按位置或索引拼接列,它压根不做"匹配"这回事。如果两个 DataFrame 的行顺序不一致、索引有缺失,拼接出来的结果就是错位的——张三的消费记录可能贴到了李四的名字旁边。你甚至很难发现这个错误,因为代码不会报错,数据也不为空。

反过来,用 merge 去拼接多个同构分片,也会制造麻烦。比如你要合并 12 个月的销售月度表,它们结构完全相同,本来用 concat 一行代码就能解决;如果改用 merge,由于 merge 没有on就不合法,强行指定一个列做连接键,就会导致行数爆炸——如果你指定的连接键有重复值,等于把每张表都拉成了笛卡尔积的规模,性能瞬间就崩了。

记住一句话:合并不等于"放在一起",concat 负责"放在一起",merge 负责"对在一起"。语义不同,底层实现和结果形态都不同,先用语义判断,再看函数文档。

2. 选型判断:五个维度决定你该用哪个 API

2.1 判断维度一:你是在"堆数据"还是在"对数据"

最直接的判断方式,是问自己一个问题:这次操作结束之后,每一行数据代表什么?

如果多个 DataFrame 的行是"同一个表的不同部分",合并后每一行依然代表原表里的一条记录,这叫堆数据——答案是 concat。典型场景:1 月销量表 + 2 月销量表 = 全年度销量表;上海门店订单 + 北京门店订单 = 全国门店订单;train_chunk_1.csv + train_chunk_2.csv = 完整训练集。

如果两个 DataFrame 的行代表不同实体,需要按某个键把信息关联起来——比如订单需要补上用户姓名,商品需要补上分类名称——这叫对数据——答案是 merge。合并后每一行包含的信息比原来任何一张表都全,但它是在"匹配"的基础上形成的。

这个维度是根本,判断清楚了,后面一切都顺了。

2.2 常见场景对照表与案例拆解

我把常见需求列成了一张速查表,遇到合并先照着过一遍:

场景推荐 API关键参数原因
同结构多表纵向堆叠pd.concataxis=0,ignore_index=True不涉及字段匹配,物理拼接最安全
不同结构表横向拼列pd.concataxis=1,join='inner'前提是索引或顺序完全对应,否则慎用
按共同字段补全信息pd.mergeon=..., how='left'关系连接,语义清晰
字段名在两侧不同pd.mergeleft_on=..., right_on=...merge 支持异名字段关联
按索引对齐DataFrame.joinhow='left'join 是 merge 的索引对齐简化版
需要交叉组合所有行pd.mergehow='cross'关系代数里的交叉连接

举一个我实际处理过的案例。当时要分析某电商平台促销活动效果,手上有三张表:activity_list(活动基础信息)、order_list(订单明细)、user_snapshot(用户画像)。三张表结构完全不同,字段也不一致。

正确做法是分两步进行:先用 merge 将订单表和活动表按activity_id关联,再用 merge 关联用户画像,每步按业务需求选择how(通常主表是订单,用left连接保证订单不漏);最后如果还要合并异常数据片段,才考虑用 concat 把多批次订单片段堆起来。

这个案例里最忌讳的,就是图省事把三张表concat(axis=1)一下,那结果基本没法用。

2.3 选错 API 的代价有多大

选错 API 的代价其实不是"报错",而是不报错但结果错误。这类 bug 在数据流水线里是最可怕的,因为你不会立刻发现,它会悄悄污染后面的统计、建模环节。

见过一个真实事故:同事做周报数据汇总时,用pd.concat([df_this_week, df_last_week], axis=1)想给本周订单"配上"上周同期的对照数据。单独看每张表,订单都是按order_id排序的,所以他默认顺序一致。结果因为上周有两次退单删除了记录,索引错位,导致本周订单配上了错误的上周金额。最终周报上的同比数据大面积出错,花了一整天才排查出来。

如果用pd.merge按order_id做how='left',就不会有这个坑——因为 merge 会精确匹配每个订单,匹配不到的自动补 NaN,一眼就能看到问题所在。

注意:凡是要"按某个标识符把两段信息对应起来"的场景,永远优先考虑 merge,而不是靠"顺序恰好一致"来赌 concat。

3. 高阶实践:把 merge 的参数用出价值

3.1 连接键的四种玩法,不止 on 一个参数

很多人用 merge 只会写on='user_id',其实连接键的玩法远不止这么简单。

第一种,多键连接。当单一字段无法唯一标识一条记录时,需要把多个字段组合成复合键。比如订单明细里,同一订单号下可能有多个商品,此时要用order_id + product_id共同作为连接键:

pd.merge( df_order_info, df_product_detail, on=["order_id", "product_id"], how="left" )

第二种,两侧列名不一致。如果左表里叫user_id,右表里叫uid,用left_on和right_on指定:

pd.merge(df_users, df_orders, left_on="user_id", right_on="uid", how="left")

第三种,用索引当连接键。某些场景下索引本身就是业务主键,此时不需要复制列,直接指定left_index=True或right_index=True:

pd.merge(df_indicators, df_targets, left_index=True, right_index=True, how="outer")

第四种,列和索引混合连接。比如左表按date列关联右表的索引(右表的索引就是日期):

pd.merge(df_events, df_calendar, left_on="event_date", right_index=True, how="left")

这四种玩法几乎覆盖了日常所有关联需求。尤其是"索引当连接键"这种用法,在很多时序数据处理场景里非常方便,理解了它,你还能自然过渡到DataFrame.join()——它本质上就是 merge 的索引对齐封装。

3.2 suffixes 与重叠列名:别再用默认的 _x/_y

两个 DataFrame 如果有一些列名相同但不是连接键,merge 的时候 Pandas 会自动给它们加后缀,默认是_x和_y。比如左表和右表都有一个amount列,合并后会出现amount_x和amount_y。

这个默认行为在生产环境里很容易引发困惑:amount_x到底是谁的?如果代码后面还要继续处理,你不得不来回翻变量定义。

我的习惯是:合并前先看清楚两边有哪些重叠列名,然后再决定要不要用 suffixes 做显式重命名。

有两种更可控的处理方式。第一种,自定义后缀,让结果列名一看就懂:

pd.merge( df_order, df_payment, on="order_id", how="left", suffixes=("_order", "_payment") )

合并后就是amount_order和amount_payment,业务含义一目了然。

第二种,更推荐的做法:在 merge 之前就把目标列改名。比如我只想保留右表的paid_amount,那就先df_payment = df_payment.rename(columns={"amount": "paid_amount"}),然后再合并,干净利落,甚至不需要 suffixes。

提示:写.merge()之前,花 30 秒打印一下两边的columns.intersection(),能省掉后面大量排查时间。

3.3 indicator 参数:一键看清合并结果到底怎么来的

indicator=True是我个人认为最被低估的 merge 参数。它会额外生成一列_merge,用三种标签标记每一行的来源:

  • left_only:只在左表出现,右表没有匹配;
  • right_only:只在右表出现,左表没有匹配;
  • both:两边都匹配上了。

直接看效果:

merged = pd.merge(df_users, df_orders, on="user_id", how="outer", indicator=True) print(merged["_merge"].value_counts())

这一行输出能瞬间告诉你两表数据的覆盖情况。比如统计结果里有大量left_only,说明右表缺失了很多用户;如果预期的全匹配结果里出现right_only,则说明有订单找不到用户——这往往是数据质量问题,值得深挖。

我经常用它做合并之后的"体检"。特别是在做特征工程合并用户画像时,我一定会看_merge的分布,如果某个画像字段缺失比例突然升高,_merge列能帮我迅速定位问题出在哪个环节。

3.4 validate 参数:把数据质量问题提前暴露

validate参数是很多专业开发者才注意到的"安全检查"工具。它接受one_to_one、one_to_many、many_to_one、many_to_many四种值,用于声明你对两侧连接键唯一性的预期。

如果你确信user_id在两张表里都是唯一的,合并后期望行数等于左表行数,那就把预期写成validate="one_to_one":

pd.merge( df_users, df_user_extra, on="user_id", how="left", validate="one_to_one" )

如果实际数据违反了这一假设——比如user_id在右表里出现了两次——Pandas 会直接抛出MergeError,而不是沉默地生成笛卡尔积。这样你就能在合并阶段立即发现问题,而不是等下游统计出错再去排查。

这个参数特别适合放进自动化数据流水线里当"断点"。数据质量一旦出现变化,脚本立刻报警,而不是带着脏数据跑完全流程。我在做 ETL 任务时几乎必加这个参数,付出的只是声明一行预期的代价,省下的却是大量排查时间。

4. concat 使用中最容易忽略的细节

4.1 ignore_index 什么时候必须加

pd.concat默认会保留原始 DataFrame 的索引,这意味着合并后的索引起止未必连续,甚至可能有大量重复。比如两个分片都从 0 开始编号,concat 之后索引就是0, 1, 2, 0, 1, 2, ...。

这种重复索引是个隐蔽的坑。后面的df.loc[1]会返回多行,如果没意识到这点,取数逻辑就可能出错。

所以,当多个分片表示同一实体、且原始索引没有业务含义时,ignore_index=True几乎总是应该加的,它会重新生成 0 到 n-1 的连续索引:

df_all = pd.concat([df_jan, df_feb], ignore_index=True)

但反过来,如果索引本身有业务含义——比如时间序列的分片,每个分片的索引是时间戳——那就不能加ignore_index,否则时间维度的信息就丢了。这种场景下反而要借用keys参数记录来源。

4.2 keys 参数:多表拼接后依然能追溯来源

keys参数是我特别推荐的一个高级用法。它会给拼接后的数据增加一层外层索引,标记每一段数据来自哪个原始 DataFrame:

df_concat = pd.concat( [df_jan, df_feb, df_mar], keys=["Jan", "Feb", "Mar"] )

合并后索引变成了二级索引,第一层是月份标签。你可以用df_concat.loc["Jan"]提取一月份数据,也可以直接df_concat.groupby(level=0).sum()按月份聚合,非常方便。

这个参数本质上是在"物理拼接"之上增加了一层逻辑分组,很适合在月度数据汇总后直接做后续分析,省掉额外加一列月份字段的操作。不过要注意,加了 keys 后索引变成了 MultiIndex,后续如果要用常规索引方式取数,可能需要reset_index()处理。

4.3 join 和 sort 参数对结果的影响

concat里还有两个不那么显眼、但影响很大的参数:join和sort。

join控制的是列对齐方式,默认'outer'取列名并集,缺失值填 NaN;改成'inner'则只保留两边都有的列。这个逻辑和 merge 里的 join 语义完全不同——concat 的 join 管的是列,merge 的 join 管的是行。二者名字相同,含义不同,非常容易混淆。

sort参数则决定拼接后是否对列名排序。默认False,也就是保留第一个 DataFrame 的列顺序;设置为True时列名按字母排序。

我自己在做数据清洗时,如果拼接的分片列顺序不一致,会先用df.columns检查一遍,再决定是否在 concat 后统一调整。否则结果表每跑一次列顺序都不一样,后续的df[["col_a", "col_b"]]虽然不受影响,但用.iloc按位置取列时会踩坑。

5. 常见问题与排查技巧实录

5.1 合并后行数莫名暴增?先查连接键的唯一性

这是 merge 最常见的"事故"。左表 1000 行,右表 1000 行,合并完却变成了 3000 行甚至上万行。原因几乎总是同一个:连接键不是唯一值,触发了多对多匹配。

举个例子,df_users中user_id是唯一的,但df_orders中一个user_id对应了多笔订单。用how="left"合并用户表和订单表时,每个用户的订单行都会被展开,结果行数自然大于左表行数。

排查方式很简单,合并前检查连接键的唯一性:

print(df_users["user_id"].is_unique) # True print(df_orders["user_id"].is_unique) # False

或者直接看重复值数量:

dup_count = df_orders["user_id"].duplicated().sum() print(f"右表连接键重复行数: {dup_count}")

明确重复情况后,再决定是validate="one_to_many"接受多行展开,还是先drop_duplicates再去关联。最怕的其实是"以为唯一、实际不唯一",所以验证这一步不能省。

注意:合并前打印每个连接键的nunique()和行数,用不了 3 秒,但能避免掉进笛卡尔积的深坑。

5.2 字段明明一样却匹配不上:数据类型在捣乱

另一种高频问题:肉眼看着两边都是user_id,合并结果却全是 NaN,或者大量匹配失败。

最常见的原因是数据类型不一致。左表的user_id是int64,右表的user_id是object(字符串),因为读取方式不同,比如一个从 CSV 读入,一个从 Excel 读入,ID 就可能在某一边被识别成了文本。更隐蔽的是:当 ID 列存在缺失值时,Pandas 在读文件时可能把它整体推断为float64,导致 1001 变成 1001.0,和另一张表的int64完全匹配不上。

排查办法非常直接,看 dtype:

print(df_users["user_id"].dtype) print(df_orders["user_id"].dtype)

修复方法也很简单,把两侧统一成同一种类型:

df_users["user_id"] = df_users["user_id"].astype(str) df_orders["user_id"] = df_orders["user_id"].astype(str)

或者统一成整数类型(前提是没有缺失值,否则要先处理 NaN)。我个人的建议是:业务主键一律转成字符串再合并。字符串没有精度问题,也不会出现1001和1001.0的尴尬差异,在关联时最稳妥。

5.3 KeyError 与 MergeError:读懂报错信息

合并报错时,报错信息其实已经把原因说得很清楚了,关键是要会读。

最常见的KeyError,一般是on、left_on或right_on里指定的列名在对应表里不存在,也就是你写错了列名。这种问题只要把代码里的列名和df.columns对照一下就能定位。

比较微妙的是MergeError。当你用on="user_id"但连接键在右表里根本不存在时,Pandas 会提示类似MergeError: 'user_id' is not in the right DataFrame;当你把合并结果赋给变量后,又尝试用_merge列做筛选,但实际没有加indicator=True,也会报KeyError。

还有一种情况容易让新手摸不着头脑:连接键在两表中都存在,但一个是普通列,一个是索引。明明看着是同一个 ID,却报KeyError。这时要检查是否应该写left_on="user_id"配合right_index=True,而不是on="user_id"。

5.4 索引重复引发的隐蔽问题

merge 按列匹配时一般不受索引影响,但concat和DataFrame.join这类依赖索引对齐的操作,对索引的重复值非常敏感。

如果你用join按索引对齐,而某一侧的索引有重复,Pandas 在合并时会产生笛卡尔积式的展开,其结果和预期往往大相径庭。更麻烦的是,这种展开很多时候不报错,只是结果行数莫名其妙地变多。

处理方式是在做索引对齐操作前,先排查索引唯一性:

print(df_left.index.is_unique) print(df_right.index.is_unique)

如果发现重复,先决定是否可以reset_index()把索引变成普通列,再走 merge 按列关联。这样至少操作对象是明确的普通列,不容易出现隐式的索引对齐错误。

6. 大数据场景下的合并性能优化

6.1 merge 慢在哪:排序与哈希连接

大表 merge 慢,一般不是 pandas 本身"偷懒",而是实现机制决定的。Pandas 的merge在底层会根据键对数据进行分组和匹配,对于大规模数据,这个过程涉及排序或者哈希表的构建,计算量和内存开销都不小。尤其当连接键有大量重复值时,匹配组合数量会急剧膨胀,直接拖慢速度。

我实测过一张 2000 万行的订单表和一张 500 万行的用户表做关联,内存峰值很容易冲破 10G,耗时也要按分钟计算。如果是在循环里反复 merge,耗时更是灾难。

理解这一点,才能有针对性地优化:核心思路是"先缩小,再合并",让参与 merge 的数据量尽量小。

6.2 四种实用的性能优化手段

第一个优化手段是先过滤再合并。如果业务上只需要某些特定条件下的数据,可以在 merge 之前先用query或布尔索引把数据裁剪掉一部分,而不是全量合并后再过滤。数据量小了,连接成本直接下降。

第二个手段是连接键排序。有时数据集对连接键是无序的,先对两表按连接键sort_values()排序,再利用有序键进行 merge,底层可以使用更高效的连接策略(比如 sort-merge join),实测在一些场景下能带来明显加速。

第三个手段是减少无用的数据列。merge 之前只保留后续分析需要的列,而不是把几百列原始数据全部拉进来。列数越少,内存占用越低,合并时复制数据的开销也越小。我见过有人把 200 列的表直接 merge,结果 80% 的列根本用不到,白白浪费内存。

第四个手段是把字符串类别化。如果连接键是高基数字符串,可以用astype('category')转换为类别类型,这样能在底层压缩存储,同时加速哈希匹配。特别是当同一个字符串在数据中反复出现时,效果非常明显。

df_users["user_id"] = df_users["user_id"].astype("category") df_orders["user_id"] = df_orders["user_id"].astype("category")

如果数据量到了千万级以上,合并次数又多,还可以考虑换用 Polars 这类按多线程设计的库(语法与 Pandas 高度相似),或者将中间结果落盘后用分块策略处理。这一步不是必须的,但当你熟悉了优化技巧仍然扛不住性能时,可以把它作为备选方案。

我自己实际写合并代码时,养成了一套固定动作:先确认业务上要"堆数据"还是"对数据",再检查两侧连接键的 dtype 和唯一性,合并前用columns.intersection()看列名重叠,合并后用indicator验证结果分布。这套流程看起来繁琐,但就是在这些不起眼的检查里,帮我避开了很多回头返工的坑。Pandas 的合并 API 本身不复杂,复杂的是数据在真实业务里的脏和乱,把基础语义吃透,比记再多参数都好用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询