
搞数据分析的兄弟pandas 用久了merge、join、concat 这三兄弟的分工你搞明白了吗坦白说我见过不少入门一两年的朋友仍然在这三个方法之间来回试。逻辑上用对了数据量小的时候无感顶多多跑几秒可用错了方向结果对不上、索引乱掉、内存暴涨这种坑我踩了不止一回。这三个方法表面上都是“把数据放在一起”但各自的底层逻辑、对齐方式、适用场景差别很大。这篇文章我用一套订单数据和客户数据结合真实业务场景把 merge、join、concat 彻底拆开讲透。不需要复杂数据集一个 DataFrame 构造完就能跟着跑。看完之后你会很清楚什么场景该选哪个方法以及为什么。1. 先说结论拼接和连接两者根本不是一回事先记住一个关键判断concat 是拼接merge 和 join 是连接。这个区分是整个 pandas 数据处理中最容易忽视、也最影响结果正确性的概念之一。拼接concat就是把两份或多份数据按顺序堆在一起像两列积木拼成一条长龙。它不关心两边有没有共同的键也不做任何匹配只做“物理堆叠”。它解决的典型问题是今天你导出了三个 CSV 片段结构完全一样想合成一张完整表。连接merge / join则完全不同。它像查户口一样拿着一个或多个键到另一张表里找对应的行然后按匹配结果拼出来。它本质上是关系型数据库里 JOIN 的翻版解决的是“我手里有订单表客户信息在另一张表怎么把客户城市拉到订单明细里”这类问题。下面这张表是我常用的对比速查表已经按高频使用场景做了精简方法本质对齐依据类似 SQL 语义默认连接方式适用场景concat沿轴堆叠无键匹配按行/列位置堆叠UNION ALL行方向 outer列取并集多份同构数据合并、追加行、追加列merge列连接指定列on / left_on / right_onJOINinner join表与表之间按业务键关联join索引/列连接默认索引也可指定列JOIN基于索引left join索引对齐、按索引合并、少量列合并注意最后一列merge 默认 inner joinjoin 默认 left join。这个差异如果你没意识到后面写出来的结果很可能就是“好像少了行又好像没少”排查半天发现是默认行为在作怪。2. merge 实战基于列的连接pandas 里的 SQL JOIN2.1 merge 的核心参数逐个拆merge 的签名其实非常长记不住没关系你只要抓住几个高频参数就够用pd.merge( left, right, howinner, # inner / outer / left / right onNone, # 两边同名的连接键 left_onNone, # 左边表的连接键列名 right_onNone, # 右边表的连接键列名 left_indexFalse, # 是否用左边索引作为连接键 right_indexFalse, # 是否用右边索引作为连接键 suffixes(_x, _y), # 同名列冲突时自动加后缀 indicatorFalse # 是否加一列记录来源 )普通业务场景里on、how、left_on/right_on、suffixes四个参数已经覆盖九成需求。来一套可以直接复制的数据后面所有示例都用这套数据展开import pandas as pd orders pd.DataFrame({ 订单号: [A001, A002, A003, A004, A005], 客户ID: [C1, C2, C3, C2, C5], 商品ID: [P1, P2, P1, P3, P2], 金额: [299, 89, 458, 129, 699] }) customers pd.DataFrame({ 客户ID: [C1, C2, C3, C4], 姓名: [张三, 李四, 王五, 赵六], 城市: [北京, 上海, 广州, 深圳] })这里订单表有 5 条记录客户表有 4 个客户。注意订单表里有一个“C5”在客户表里并不存在这就是后续排查 left / inner 区别的关键素材。2.2 四种 how 模式一次看明白先做最常用的 inner内连接res pd.merge(orders, customers, on客户ID, howinner) print(res.shape)结果订单表里有 C1、C2、C3客户表里都有所以 4 条订单能匹配上A005 订单对应的 C5 在客户表没有直接丢掉。inner 只保留两边都匹配得上的行这是它和 SQL inner join 一致的地方。再看 left左连接我日常用得最多的一种res pd.merge(orders, customers, on客户ID, howleft)结果保留了订单表全部 5 行C5 那行客户表的“姓名”“城市”填充为 NaN。这里有个重要理念业务上“订单是主表”客户信息是“补全信息”订单一条都不能少所以必须用 left。很多数据分析新手把订单关联客户写成 inner导致订单行数变少后面对账怎么都对不上就是这一步出的问题。right 和 outer 同理右连接保留右表所有行全外连接保留两边所有行没有匹配的字段用 NaN 填充。搞清楚“谁保留全部行”就抓住了 how 的本质。2.3 连接键列名不一致怎么办现实数据永远没有教程里那么干净。订单表里叫“客户ID”客户表里叫“客户编号”这时候用left_on和right_oncustomers2 customers.rename(columns{客户ID: 客户编号}) pd.merge(orders, customers2, left_on客户ID, right_on客户编号, howleft)结果里会出现两列客户ID 和客户编号。客户编号列其实就是给匹配用的副本分析时通常用.drop(columns客户编号)把它清理掉或者 merge 后用.loc挑选需要的列。这里还有个快捷键当两边键都对应索引时可以直接开启left_indexTrue和right_indexTrue绕开列名不一致的问题但这种场景更建议用下一节的 join。2.4 一对多和多对多不要被结果“变多”吓到merge 还有一个让很多人困惑的行为如果两边的键都出现多次结果行数会膨胀。举个例子订单表里 C2 下了两笔单客户表里 C2 只有一条关联后 C2 的客户信息会复制到两行这就是一对多。# 客户表追加一条重复客户ID模拟多对多 dup_customers pd.DataFrame({ 客户ID: [C1, C1, C2], 姓名: [张三, 张小三, 李四], 城市: [北京, 北京, 上海] }) pd.merge(orders, dup_customers, on客户ID, howleft)因为这个 dup_customers 表里 C1 出现了两次而订单表里 C1 只有一笔所以那笔订单会生成两行这就是多对多导致的笛卡尔积式膨胀。merge 本身不帮你识别“这个键是否唯一”你必须在业务上确认连接的结果预期是一对一还是一对多如果列不唯一且不希望膨胀先聚合再去 merge或者明确使用validate参数pd.merge(orders, dup_customers, on客户ID, howleft, validateone_to_many)validate会自动检查连接关系是否符合预期不符合时直接抛异常。我之前在一次销售明细汇总里就是因为没有加这个校验结果行数莫名翻倍排查了大半天。现在数据量大的 join 操作我都会加上。3. join 实战默认走索引的左连接3.1 join 的通用语法join 与 merge 最大的区别在于默认连接键是索引index。语法上它是 DataFrame 的实例方法orders.join(othercustomers, on客户ID, howleft)或者更常见的写法先把客户表按索引设置好customers.set_index(客户ID, inplaceTrue) orders.join(customers, on客户ID, howleft)注意这里 orders 自身的索引还是 0 到 4没有变化join 通过on参数指定“用 orders 的客户ID 列去匹配 customers 的索引”。如果不写on比如默认情况orders.join(customers)那 pandas 会尝试用 orders 的索引和 customers 的索引对齐两边恰好都是 RangeIndex 时结果就是按位置硬对齐这在业务场景下几乎没有意义。3.2 为什么 join 默认是 left join这是所有教程里最容易被跳过、但实际最有价值的一点。merge 默认是 inner因为 SQL 的 JOIN 默认就是 inner而 join 是 DataFrame 自己的方法面向的对象往往是一个“主表”顺序上通常就是“我要在这个表上补信息”所以 pandas 把默认设成 left join避免主表行数减少。我用 join 最多的时候是三张以上表要按同一个索引拼信息。比如按客户 ID 索引把客户详情、会员等级、消费积分三张表一次性拼到订单表上orders.join( [customers.set_index(客户ID), membership.set_index(客户ID), points.set_index(客户ID)], on客户ID, howleft )这个写法比连续 merge 三次干净太多。join 可以传一个列表一次合并多张表。如果需要合并的表都共享同一个索引连on都不用写customers.set_index(客户ID, inplaceTrue) membership.set_index(客户ID, inplaceTrue) orders.set_index(客户ID, inplaceTrue) orders.join([customers, membership], howleft)这样三个表都按客户ID作索引直接一拼到位。注意这种做法会把 orders 原来的“订单号”等列保留索引变成客户ID如果需要还原最后reset_index()即可。3.3 join 和 merge 的关系不是“谁替代谁”实际上 join 在 pandas 内部实现上是基于 merge 的封装但有几个行为差异join 的默认连接键是索引merge 的默认连接键是同名列。join 可以一次合并多个 DataFramemerge 一次只能合并两个。join 默认是 leftmerge 默认是 inner。所以如果你对着一张“键在索引上”的表做关联join 的代码更短如果你主要靠业务列关联且需要精细控制连接键名称merge 更灵活。选谁不是看谁更高级而是看你的数据结构和可读性要求。4. concat 实战沿轴的堆叠一次把多份数据合一起4.1 axis0 和 axis1 到底怎么理解concat 的核心参数是axis。axis0表示沿着行的方向拼接也就是上下堆叠要求两张表的列结构一致axis1表示沿着列的方向拼接也就是左右拼宽要求两张表的行一一对应。这句“一一对应”太重要了很多人把 axis1 当成 merge 来用但 concat 根本不检查行与行之间是否匹配它只是把左边 DataFrame 和右边 DataFrame 相同位置的 row 硬拼在一起。如果你的两张表行顺序一致比如分别是同一个批次的订单金额和退款金额那 axis1 没问题如果行顺序完全不一致哪怕索引一致它也会按位置硬拼接结果直接错乱。来看一段代码orders_jan pd.DataFrame({ 订单号: [J001, J002], 金额: [199, 599] }) orders_feb pd.DataFrame({ 订单号: [F001, F002, F003], 金额: [299, 399, 99] }) # 行方向堆叠 pd.concat([orders_jan, orders_feb], axis0, ignore_indexTrue)输出 5 行就是简单地接在后面。因为 orders_feb 的订单号以 F 开头根本不存在键匹配这种概念。这才是 concat 的正确使用场景同构表量多份想拼成一份。4.2 join 参数以及 ignore_index 的坑concat 的join参数控制“轴之外的另一个方向”如何处理。默认是outer也就是保留两边所有列缺失的列用 NaN 填充orders_extra pd.DataFrame({ 订单号: [F004], 金额: [49], 备注: [退货暂缓] }) pd.concat([orders_jan, orders_extra], axis0)注意orders_extra 多了一个“备注”列默认 concat 会把 备注 列保留前面几行都填 NaN。这种“自动补列”在很多时候很省事但它隐藏了一个风险如果两边列名拼写稍有不同比如一边是“金额 ”一边是“金额”全角和半角空格concat 不会报错而是悄悄给你生成两列数据直接分家。所以我强烈建议 concat 之前统一检查列名。ignore_indexTrue则更常见。concat 默认会保留每个 DataFrame 原来的索引下标会重复。比如 orders_jan 的索引是 0、1orders_feb 的索引也是 0、1、2拼接后就会出现两个 0、1还有 2。为后续筛选、分组带来麻烦通常拼接后要reset_index(dropTrue)或者直接 concat 时传ignore_indexTrue一步到位。4.3 keys 参数分组拼接两不误concat 里还藏着一个好用的参数keys。它可以在拼接同时给每份数据打上标签最终生成多层索引merged_all pd.concat( [orders_jan, orders_feb], ignore_indexFalse, keys[1月, 2月] ) merged_all结果索引会变成多级索引第一级是“1月”、“2月”第二级是原来的索引。这样后续merged_all.loc[1月]就能直接取出 1 月的数据非常利于这种按月份追加的场景。4.4 concat 与 merge 的本质差异再强调上一节讲过concat 是做堆叠merge 是做匹配。我举一个典型错误案例有一个“订单主表”和一个“退款表”你想把退款金额填到订单上。如果直接用 concat axis1订单主表 1000 行退款表只有 120 行结果会怎么拼它会按位置拼第 121 行往后退款金额全是 NaN而且退款表没有的订单反而对齐到了错误的位置。正确做法是用 merge 按订单号关联或者把退款表按订单号索引后用 join。一个简单的记忆口诀如果列方向是对齐拼接用 concat如果是按业务键把一张表的值填到另一张表用 merge 或 join。5. 场景选择指南遇到真实业务这么选5.1 高频场景速查我在表格里花了点心思把平时见得最多的几类需求直接对应到了方法业务需求推荐方法原因多份同结构 CSV 合成一张表concat(axis0) ignore_index结构一致纯堆叠按订单号关联客户信息merge(on订单号, howleft)按业务键匹配保留主表按客户ID索引关联多张维表join(howleft)多表连续合并代码简洁同一批订单宽表两侧字段补齐concat(axis1)行顺序一致纯横向展开订单表和退款表按订单号配对merge(on订单号, howouter)需要键匹配可能出现一对多调查数据分组汇总后合并展示concat(keys[组1,组2])需要分组标签后续按组切片5.2 一个综合案例订单、客户、退款三表关联用一个完整案例收拢前面的知识。假设手头有订单表、客户表、退款表三份数据目标是输出一份包含订单明细、客户城市、退款金额的宽表。orders pd.DataFrame({ 订单号: [A001, A002, A003, A004], 客户ID: [C1, C2, C3, C2], 金额: [299, 89, 458, 129] }) customers pd.DataFrame({ 客户ID: [C1, C2, C3, C4], 姓名: [张三, 李四, 王五, 赵六], 城市: [北京, 上海, 广州, 深圳] }) refunds pd.DataFrame({ 订单号: [A002, A004], 退款金额: [89, 129] }) # 第一步订单关联客户保留全部订单 step1 pd.merge(orders, customers, on客户ID, howleft) # 第二步订单关联退款退款存在时才配对 final pd.merge(step1, refunds, on订单号, howleft) final这里第二步不能替换成 concat因为退款表只覆盖部分订单而且按订单号匹配concat 会把退款表整体“接下面”而不是“填进来”。用 left join 能干净地保留全部订单没退款的订单退款金额为 NaN。如果业务要“只展示有退款的订单”再把第二步改成 inner join 即可。5.3 性能参考与取舍数据量大了以后性能也是一个维度。我的实测经验是底层逻辑上 merge 是 C 实现的 hash join比纯 Python 循环快至少两个数量级join 因为内部也走 merge性能接近。concat 则更轻量一些因为它不涉及键匹配只做内存块的复制和拼接。有一个实际优化技巧如果两张表都是千万级merge 之前最好先把参与连接但不参与结果的列 drop 掉减少内存拷贝。另一个常见建议是在 merge 前确保连接键为合适的类型比如把“客户ID”统一成字符串否则两边一个 int 一个 object 会导致匹配失败虽然结果会按 NaN 填而不是报错但你已经踩坑了。6. 常见问题与排查实录6.1 合并后行数“变多”或“变少”先确认主键唯一性这是咨询频率最高的坑。inner 连接行数变少是预期行为left 连接行数变多大概率右表连接键重复每个匹配都复制了一次。排查方法很简单在合并之前对右表连接键做value_counts()看是否有重复值。customers[客户ID].value_counts()如果重复存在考虑先drop_duplicates(subset客户ID)再去 merge。业务上如果右表同一键对应多条信息还要想清楚保留哪条比如按日期取最新。6.2 同名列自动加后缀偶尔会藏脏数据merge 时两边有同名列但不作为连接键比如订单表里有“备注”退款表里也有“备注”结果会出现“备注_x”和“备注_y”。很多人会忘记清理这两列后面处理时用的是“备注_x”却以为自己在看合并后的“备注”。我的习惯是 merge 之后立刻print(result.columns.tolist())检查常用的列名确认没有意外后缀再继续分析。如果你对后缀有明确偏好可以自定义pd.merge(orders, refunds, on订单号, howleft, suffixes(_订单, _退款))这样结果里会出现“备注_订单”、“备注_退款”比默认的“_x”、“_y”可读性好很多。6.3 concat 后行数和索引都好但 DataFrame 还是“翘了”有时候 concat 之后看着数据没问题但用于透视或分组时很多操作突然报错或者结果奇怪。这通常是因为各分片 DataFrame 的列顺序不一致concat 按列名对齐可能导致列顺序重排或者有的分片有空 DataFrame、列全部 NaN导致 dtype 变成了 object。解决方法是明确sortFalse固定列顺序并在 concat 后用.info()检查 dtype 是否异常。如果每批次数据的列数量一致但列名顺序不同建议 concat 前统一.reindex(columns标准列顺序)保证对齐逻辑稳定。6.4 索引唯一性被破坏join 结果出现意外笛卡尔积join 按索引连接如果右表索引有重复结果会和 merge 的多对多行为一样膨胀。这个问题特别隐蔽因为 set_index 之后你不会一眼看出索引是否有重复。我会在 join 前加一个断言级别判断assert not customers.index.duplicated().any(), 索引存在重复请先清理至少三张表一起 join 时这个 assert 能救我一命。6.5 嵌套列和 MultiIndex 的细节问题如果 DataFrame 本身带着多层索引或列名是 MultiIndexconcat 和 merge 的行为都会变得更复杂。比如 concat 多份 MultiIndex 列的数据默认内层列名不一致会用 NaN 补齐。一个可行的兜底方案是把所有 DataFrame 统一做reset_index()、拍平列名再走常规的 merge 或 concat。虽然放弃了一部分 MultiIndex 的高级特性但代码鲁棒性高很多特别是数据来源不稳定时。7. 一点个人实操心得用这三个方法这几年我最大的体会是不要靠“试”来选方法要靠在动手前先问自己两个问题。第一个问题是“数据之间有没有键需要匹配”有就是 merge 或 join没有就是 concat。第二个问题是“我的键在索引上、还是在一列里”在索引上优先 join在列里优先 merge。另外一个小习惯可能对你有用每次做表关联之前先记录左表的行数关联之后再记录一次。如果行数变了你第一时间就能知道连接关系不是一对一这一步检查只需两秒但能挡住大多数合并带来的脏数据事故。最后代码里把how参数写明确不要依赖默认值。哪怕你要的刚好是 inner也写上howinner三个月后回来看代码不会再去猜当时的意图。同样的道理concat 里明确写axis0、joinouter。代码可读性和结果正确性有时候是一回事。