ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Pandas数据合并实战:彻底搞懂merge与join的用法

Pandas数据合并实战:彻底搞懂merge与join的用法 做数据分析的人大概率都经历过这个场景手里有两张表一张是订单明细一张是用户信息订单表里有user_id用户表里也有user_id但订单表里没有用户姓名和城市你想按城市分析购买习惯第一步就得把两张表拼起来。这个动作在 Excel 里叫 VLOOKUP在 SQL 里叫 JOIN在 Pandas 里就是merge和join。很多初学者学到数据合并这一章会觉得 Pandas 怎么突然变难了。前面创建Series、DataFrame、做切片、做索引都还直观一到merge这里参数一下子冒出来一堆on、how、left_on、right_on、left_index、right_index、suffixes……每个参数单独看都懂连在一起就不知道怎么组合。更麻烦的是合并完行数有时候变多有时候变少有时候直接翻了好几倍完全搞不清 Pandas 到底在干什么。这篇文章我就从实际使用的角度把merge和join彻底拆开讲清楚。不堆概念直接看代码、看结果、看场景。适合正在学 Pandas 做数据预处理的人也适合工作中经常要手动整理表格的分析师。你不需要把所有参数背下来只需要理解它们背后的逻辑以后遇到任何合并需求都能自己推出来怎么写。1. 先弄明白merge 和 join 是同一件事的两种叫法很多人第一次看到merge和join这两个方法时都会纠结一个问题它俩到底有什么区别我到底该用哪个网上教程也是各讲各的有的标题写merge有的标题写join内容却差不多看得人更糊涂。1.1 它们不是两套独立的 API直接说结论join是merge的简化封装它的底层调用的就是merge。你去翻 Pandas 的源码会发现DataFrame.join()本质上就是帮你把连接键设置成了索引然后调用pd.merge()。怎么理解这件事你可以把merge当成一个功能完整的工具箱而join是工具箱里专门用来处理按索引合并的那把顺手螺丝刀。当你需要连接两个表时merge什么都能干而join只专注于用索引做连接键这一个场景写起来更短、更快。举个例子你有一个订单表df_orders索引正好是用户ID还有一个用户表df_users索引也是用户ID。用merge写是这样的import pandas as pd df_orders pd.DataFrame( { 商品: [鼠标, 键盘, 显示器], 金额: [99, 299, 899], }, index[101, 102, 101], # 索引是用户ID ) df_users pd.DataFrame( { 姓名: [张三, 李四], 城市: [北京, 上海], }, index[101, 102], # 索引也是用户ID ) # 用 merge需要明确指定 left_index 和 right_index result pd.merge(df_orders, df_users, left_indexTrue, right_indexTrue)用join写是这样的result df_orders.join(df_users)看到区别了吗join默认就用索引对齐不需要额外传参数。这就是简化封装的含义——它不是另一个功能而是同一个功能的快捷入口。1.2 什么时候该用哪个我个人的使用习惯是只要连接键是普通列就用merge因为on参数写起来直白只要连接键是行索引就用join因为省事。如果你要连接的键在左边是列、右边是索引或者反过来那merge的left_on、right_index组合能覆盖所有情况但写起来稍微啰嗦一点。如果把两者的关系画成一张参数对照表大概是这样的场景merge 写法join 写法两边都是普通列列名相同pd.merge(a, b, onid)需要先set_index两边都是索引left_indexTrue, right_indexTruea.join(b)左边是列右边是索引left_onid, right_indexTruea.join(b.set_index(id))两边列名不同left_ona_id, right_onb_id需要先rename或set_index所以你在网上看到有些文章说用 join 就行merge 是多余的或者反过来其实都不准确。它们不是竞争关系而是同一个功能在不同场景下的两种表达方式。先把merge的参数吃透join自然就会用了。2. 四种连接方式对应四种业务需求merge最核心的参数是how它决定了合并时保留哪些行。Pandas 提供四种方式inner、left、right、outer。这四种方式不是让你背下来的而是对应四种不同的业务场景。你只要想清楚我到底要保留哪些数据代码自然就写出来了。2.1 inner只要两边都能匹配上的howinner是默认值意思是最取交集。左表有的记录右表必须也找得到对应的连接键否则这一行就不会出现在结果里。用刚才的订单表和用户表举例import pandas as pd orders pd.DataFrame({ 订单号: [1, 2, 3, 4], 用户ID: [101, 102, 101, 103], 金额: [99, 299, 899, 199], }) users pd.DataFrame({ 用户ID: [101, 102, 104], 城市: [北京, 上海, 广州], }) result pd.merge(orders, users, on用户ID, howinner) print(result)输出结果里用户ID 为 103 的订单不会出现因为用户表里没有 103 这个用户用户ID 为 104 的用户也不会出现因为他没有下过单。inner只保留两边都对得上的行。这在业务上对应什么场景最常见的用法是只分析有完整信息的记录。比如你想统计每个有效订单的用户画像如果一个订单找不到用户资料它本身就有数据质量问题干脆不纳入分析。这种时候用inner反而是最安全的。2.2 left / right以一边为主另一边能匹配就匹配匹配不上留空比inner更常用的其实是left。它表示左表每一行都保留右表能找到对应值就填进来找不到就填 NaN。result pd.merge(orders, users, on用户ID, howleft) print(result)结果里订单号 4 依然在但它的城市是NaN因为用户表里没有 103 号用户。这对应业务里最常见的主表补字段需求我有一个主表我不希望主表里的任何一行因为匹配不上而消失我只是想在它的右边多补几列信息。订单号 4 虽然找不到用户城市但它本身是一条真实存在的订单记录不能丢。right和left是对称的只是以右表为主。实际工作中right用得少因为大多数时候我们写的代码左边是主表右边是拿来补信息的表。但如果你的主表在右边用right可以少写一次调换位置的代码。2.3 outer全都要缺的地方补空howouter是并集两边所有的行都会出现在结果里。左表有右表没有的或者右表有左表没有的都保留匹配不上的列填 NaN。result pd.merge(orders, users, on用户ID, howouter) print(result)这种方式的典型场景是做全量对比。比如你想知道两个系统的用户名单差异左表是 A 系统的用户右表是 B 系统的用户outer能把两边所有的用户都列出来哪边缺失一目了然。同时它也是检查数据质量的好帮手——如果outer的结果里出现大量 NaN说明两边数据源的覆盖范围差别很大你得找出原因。2.4 快速判断用哪种的一句话口诀刚开始用 Pandas 的时候每次都要想一下该用哪个how。后来我总结了一个判断方法基本上套进去就不会错只想要两边都匹配上的记录 →inner左表是老大不允许少一行右表只是来补充信息的 →left右表是老大不允许少一行 →right两边的记录我都想知道全貌缺失的行也想看到 →outer一句话版本inner 要交集outer 要并集left/right 要保留某一边的全部行。记住这句话四种方式就再也不会混了。实际项目里left和inner占了九成场景outer偶尔用right相对少一些。3. 连接键的指定方式on、left_on、right_on、索引how决定保留哪些行而连接键决定按什么对齐。Pandas 里指定连接键的方式有几种新手最容易在这块卡住。其实拆开看每一钟都对应一种数据结构情况。3.1 on两边列名相同最省事的写法如果两个表里要连接的列名字一模一样直接用on就行pd.merge(orders, users, on用户ID)这个是最常见的写法。on可以传一个字符串也可以传一个列表比如on[用户ID, 订单日期]表示用多个列一起作为连接键。多列连接在后面单独说。3.2 left_on right_on列名不一样时的组合拳现实中的数据很少那么规整。可能订单表里叫user_id用户表里叫id这时候 Pandas 并不知道它们是同一个东西你需要手动告诉它pd.merge(orders, users, left_onuser_id, right_onid)注意这样合并完以后结果里会有两列一列是user_id一列是id它们的值是相同的。如果你不想要id那一列合并之后还得手动drop掉。这也是为什么很多有经验的 Pandas 用户会在合并前先把列名统一成一样的再直接用on——虽然多一步rename但后续逻辑更清晰也不容易留下冗余列。3.3 多列连接复合连接键数据表里经常出现单靠一列无法唯一定位一行的情况。比如一个表用year和month两个字段才能确定某个月的数据另一个表也是按月份记录的那你连接时就该把两列都作为连接键sales pd.DataFrame({ year: [2024, 2024, 2025], month: [1, 2, 1], 销售额: [100, 200, 300], }) target pd.DataFrame({ year: [2024, 2025], month: [1, 1], 目标: [150, 250], }) result pd.merge(sales, target, on[year, month], howleft)这里如果不把year和month一起作为连接键而是只连接year那 2024 年 1 月和 2 月的记录都会匹配到同一个目标值导致数据重复。复合连接键本质上就是两个字段联合起来作为唯一标识和数据库里的联合主键是一个意思。3.4 left_index / right_index用行索引对齐有些时候你要连接的字段不在列里而在索引里。比如你之前用groupby对数据做了分组聚合groupby的键变成了索引你现在想把这个聚合结果和原表合并就得用到索引连接orders_grouped orders.groupby(用户ID)[金额].sum() result pd.merge(orders, orders_grouped, left_on用户ID, right_indexTrue, howleft)left_on用户ID表示左边按列连接right_indexTrue表示右边按索引连接。这个组合在groupby之后非常常用你已经算好了每个用户的消费总额现在要把总额填回到订单表的每一行。如果你两边都是索引直接用join更省事。比如前面那个例子df_orders.join(df_users)所以我的建议是merge你必须掌握因为它能处理所有情况join你可以把它当成只用索引合并时的快捷方式不需要有心理负担。4. 合并结果的列名冲突suffixes 和重复列有一种情况合并前没想到看到结果才发现怎么多出来两列长得差不多的——两个表里都有一列叫金额merge之后 Pandas 会自动给它们加上后缀变成金额_x和金额_y。4.1 默认后缀 _x 和 _y 的含义Pandas 的merge在连接键之外的列名发生重复时默认会在左表列名后面加_x右表列名后面加_y。这不是 bug而是一种安全机制——它避免了两列数据被无声无息地覆盖掉。比如订单表里有个金额优惠券表里也有个金额两个金额含义完全不同如果不做区分直接合并数据就乱了。所以 Pandas 宁可多两列也不牺牲正确性。但你如果直接把结果拿去做分析很容易踩坑你以为金额只有一列实际代码里取出来的df[金额]会报错因为根本不存在这一列了。这种情况新手特别容易懵。4.2 用 suffixes 自定义后缀默认的_x、_y太抽象有时候看不出哪列来自哪张表。你可以通过suffixes参数指定更可读的后缀result pd.merge( orders, discounts, on订单号, howleft, suffixes(_订单, _优惠) )这样结果里就是金额_订单和金额_优惠一眼就能看出来自哪张表。我个人的习惯是在正式的数据处理脚本里永远不要用默认的_x、_y因为它含义不明而suffixes只需要多写一个参数可读性提升一大截。4.3 合并完之后的列整理合并只是第一步真正拿到结果后你通常还得打扫战场删掉多余的列、给列重命名、调整列顺序。这是一个很常见的后续流程result result.rename(columns{金额_订单: 订单金额, 金额_优惠: 优惠金额}) result result.drop(columns[优惠金额]) # 如果这列用不上有些教程会教你用drop删除但我的建议是如果一列真的不需要最开始就不要让它出现在结果里。办法是合并之前先用[]把右表筛选成只包含连接键和需要的列pd.merge( orders, discounts[[订单号, 优惠金额]], # 只取需要的列 on订单号, howleft )这样merge之后不会产生你不需要的列根本不存在删除的问题。先筛选再合并比合并完再删列要清爽得多也少很多出错的隐患。5. 行数爆炸与重复键合并结果为什么和你想象的不一样用merge的新手几乎都会遇到一个诡异问题合并前左表 1000 行右表 500 行合并完结果竟然变成 3000 行甚至 5000 行。他们第一反应是Pandas 是不是出 bug 了。其实 Pandas 没出 bug是你的数据里有重复的连接键。5.1 一对多合并行数变多的正常情况先说一种正常情况。左表每一行在右表里能找到多条匹配记录时结果的行数就会变多。我举一个极端的例子订单表里有一条订单对应的用户表里同一个用户ID出现了两次比如因为用户表里有重复记录那么合并后这一条订单会变成两行。这在业务上是有意义的如果你在左表存的是订单右表存的是订单商品明细一个订单有多件商品合并后每个商品占一行行数变多是应该的。这叫一对多合并不是错误。5.2 多对多合并笛卡尔积灾难最危险的是多对多。左表里同一个连接键出现了 3 次右表里同一个连接键也出现了 2 次合并结果里就会出现 3 × 2 6 行。这就是数学里的笛卡尔积。打个比方A 班有 3 个同学姓王B 班的名单上也恰好有 2 个姓王的同学。如果你按姓去合并两个班级的名单这 3 个和 2 个会两两组合出 6 种匹配关系其中大部分都是错误配对。真实数据里如果连接键不是唯一键这个问题会非常隐蔽——你不会立刻发现结果错了只会在后面统计时发现数据翻倍了。检查重复键的办法很简单合并之前先跑一下# 检查左表 print(df_left[用户ID].duplicated().sum()) # 检查右表 print(df_right[用户ID].duplicated().sum())如果duplicated().sum()大于 0说明这个表连接键有重复你要思考两个问题第一这个表是不是本身就有脏数据需要清洗第二合并之后行数膨胀是否符合你的预期。5.3 合并前先确认行数变化是否符合预期我处理数据合并时的习惯是合并之前先记下两个表的行数合并之后再对比一次print(len(df_left), len(df_right)) result pd.merge(df_left, df_right, on用户ID, howleft) print(len(result))如果行数变化和预期不符我一定会停下来查清楚原因而不是糊里糊涂地拿着合并结果去做后续分析。这里有个很重要的经验合并操作本身不会报错但逻辑上的错误它也不会提醒你。你只能靠自己对数据行数的敏感度来兜底。还有一点inner合并之后行数如果比左表还多通常意味着右表连接键有重复left合并之后行数如果比左表多那几乎可以肯定是右表连接键有重复或者右表存在一对多关系。这个判断逻辑记下来排查数据问题时会快很多。6. 实战演练三张表合并的完整流程理论知识讲完了我带你用一个接近真实的场景把整个流程走一遍。假设你手上有一个订单表一个用户表一个地区表你想做一份按城市统计订单金额的报表。但是订单表里只有用户ID没有城市要拿到城市得先通过用户表找到用户所在的城市。6.1 第一步读数据、看结构先模拟三张表import pandas as pd orders pd.DataFrame({ 订单号: [1001, 1002, 1003, 1004, 1005], 用户ID: [1, 2, 1, 3, 4], 金额: [99, 299, 899, 199, 159], 日期: [2024-01-01, 2024-01-02, 2024-01-03, 2024-01-04, 2024-01-05], }) users pd.DataFrame({ 用户ID: [1, 2, 3], 姓名: [张三, 李四, 王五], 地区ID: [101, 102, 103], }) regions pd.DataFrame({ 地区ID: [101, 102, 103, 104], 城市: [北京, 上海, 广州, 深圳], })拿到表之后我习惯先看三件事表的行数、连接键是不是有重复、每列的数据类型。直接用info()和head()看一圈心里有底了再动手。6.2 第二步订单表左连接用户表订单表是主表每一笔订单都要保留所以用howleftstep1 pd.merge(orders, users, on用户ID, howleft) print(step1)此时订单号 1005 对应用户ID 4在用户表里不存在所以它的姓名和地区ID都是 NaN。这是正常的后面可以用dropna()把没有用户信息的订单筛掉也可以填一个未知占位符。如果你连用户ID 4 为什么不在用户表里这件事都想弄清楚可以用outer独立检查一次看哪些用户没有订单哪些订单没有用户。但这里我们只做报表不深挖数据治理问题所以left就够用了。6.3 第三步再连接地区表step1结果中有地区ID地区表里有地区ID这次还是一对一连接。注意这里地区表只有 4 行用left去连地区ID 对应的城市如果找不到就会是 NaNstep2 pd.merge(step1, regions, on地区ID, howleft) print(step2)到这里订单表、用户表、地区表的信息就都在一张表里了。然后你就能很自然地做分组统计report step2.groupby(城市, dropnaFalse)[金额].sum().reset_index() print(report)dropnaFalse的意思是城市为空的那笔订单也要出现在结果里方便你发现数据缺口然后再决定怎么处理。6.4 merge 结果作为中间表继续操作很多人在做多表合并时喜欢一行代码把三张表全连完。Pandas 的merge一次只支持两个表所以多表合并本质上就是把上一次合并的结果当作下一次合并的左表一步一步来。你也可以用reduce函数把多张表串起来但我不建议一上来就用那种高阶写法——可读性会变差排查问题也更困难。我的建议是每一步的中间结果都存成一个变量并且打印出来看一眼。虽然多写几行代码但每一步都验证过在后面发现问题时能快速定位是哪一步出了问题。7. 避坑清单merge/join 使用中最容易犯的 5 个错误最后分享几个我实际使用中踩过的坑。每一个都经历过结果看起来没问题实际上已经错了的教训希望你遇到时能少走弯路。7.1 连接键的数据类型不一致这是最隐蔽的坑。两个表里表面上都是用户ID但一个表是整数类型int64另一个表是字符串类型object。合并时 Pandas 不会报错但你会发现几乎匹配不上结果全是 NaN。检查方法很简单print(orders[用户ID].dtype) print(users[用户ID].dtype)如果类型不一致先统一再合并users[用户ID] users[用户ID].astype(int)这个坑之所以隐蔽是因为你用眼睛看数据时两列值长得一模一样根本没意识到背后的类型不同。尤其当你从 Excel 读数据时Excel 里的数字列有时候会带小数点、空格读进来类型就变了。所以拿到外部数据dtype一定要看一眼。7.2 连接键里有 NaN如果连接键本身有缺失值merge时这些行是匹配不上的。更麻烦的是如果右表连接键里也有 NaN所有 NaN 之间也不会互相匹配——因为NaN ! NaN。这在 SQL 里也是一样的逻辑NULL 不等于 NULL。处理方式要看业务如果连接键缺失的行对你没有意义直接过滤掉如果有意义先填充一个占位值再去合并。千万别稀里糊涂地以为缺失值之间会自然对上。7.3 合并时没注意连接键是否唯一前面第四部分讲过的笛卡尔积问题我在这里再强调一遍。合并前用duplicated()检查一下连接键唯一性成本极低却能在早期就拦住大量错误。尤其是做一对多合并时右表如果有重复键结果的行数会悄悄膨胀你在下游做统计时也会跟着算错。7.4 合并完忘记处理索引merge默认会生成一个新的连续整数索引左表原来的索引不会保留。有时候你在前面做了一堆set_index、分组操作合并完索引被重置了后面想按索引取数时发现老索引找不到了。如果合并后你还需要原来的索引合并前先把索引reset_index()变成普通列或者合并后自己再手动set_index设置回来。这个细节不会导致数据错误但会浪费不少排查时间。7.5 把 merge 当成了 Excel VLOOKUP 的无脑替代最后说一个思维方式上的坑。Excel 的 VLOOKUP 默认行为是找到第一个匹配值就返回结果哪怕右表有多个匹配它也只会取第一个。但 Pandas 的merge会把所有匹配都返回。所以如果你把一个右表有大量重复键的数据拿去做 VLOOKUP 思维下的合并结果一定比你预期的多出很多行。正确做法是先弄清右表的连接键是否唯一。如果不唯一想清楚你要的是取第一条还是全部保留前者需要提前做去重后者直接用merge即可。说到底合并不是碰运气而是明确知道连接键的基数关系一对一、一对多、多对多之后再有针对性地写代码。我在实际项目里合并过千万行级别的订单数据和用户数据也合并过只有几十行的配置表。经验告诉我merge这个操作本身不复杂复杂的是数据里的各种意外情况。花五分钟检查连接键的类型、重复情况和缺失情况能省掉后面好几个小时的排查时间。把这一步养成习惯你的数据处理能力会比大多数只会调参数的人扎实得多。
返回列表