ARTICLE DETAIL

资讯详情

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

Pandas数据合并:深入详解merge与join的核心用法与实战

Pandas数据合并:深入详解merge与join的核心用法与实战 先说我观察到的现象很多人学Pandasgroupby用得飞起concat也知道是竖着拼可一到要把两张表“横着”拼到一起就开始原地发呆——有人用for循环一条一条去查有人把两个DataFrame硬塞进一个字典还有人直接concat然后祈祷列能自动对齐。每次看到这种代码我都想按着对方的肩膀晃一晃醒醒merge和join就是专门干这个的。这一章讲的是Pandas里的横向数据合并也就是把多张表按某个关键字段或者索引拼接成一张宽表。业务里最典型的场景订单表和客户表要对上行情数据和财务数据要按日期对齐做特征工程时把三张特征表合成一个训练集。不管你是做数据分析、数据清洗还是给机器学习准备数据这都属于每天都要用的常规操作。有些朋友搜“merge”的时候会先撞上Git的合并操作那个场景完全不同咱们这里只聊Pandas里的数据合并。要是你还没装好Pandas命令行里pip install pandas或者用PyCharm打开Settings - Project - Python Interpreter点加号搜pandas安装装完再往下看。1. 先搞懂merge到底在做什么1.1 为什么需要“横向”合并Pandas里最常被拿来和merge对比的是concat。concat擅长的是“叠放”要么把两个DataFrame上下堆在一起axis0要么左右并排摆在一起axis1。问题在于它不管你两张表之间有什么业务关系纯粹按照位置或者索引标签去堆。你可以把客户表和订单表concat起来但得到的结果只是两张表物理上拼在一起每个订单并不会自动对应上它自己的客户信息。这就引出了真正的需求按照一个“键”去匹配数据。比如订单表里有个customer_id客户表里也有个customer_id我希望能把客户姓名、城市这些信息直接填到每个订单旁边。这种操作在数据库里叫JOIN在Pandas里就是merge。我经常跟初学者打比方你要合并的人不是“排队站在一起”而是“按身份证号对号入座”。merge干的事情就是把相同键值的行匹配起来然后把右边表的其他列并到左边来。1.2 merge的底层逻辑键匹配与连接方向先把最核心的三个概念讲清楚左表、右表、连接键。merge的语法是left.merge(right, on键, how方向)其中left就是左表right就是右表on告诉Pandas按哪一列来匹配how决定匹配不到的行怎么处理。how有四个候选值分别是inner、left、right、outer。我用两组极简数据演示一下import pandas as pd orders pd.DataFrame({ order_id: [1001, 1002, 1003, 1004], customer_id: [1, 2, 1, 3], amount: [199, 299, 159, 499] }) customers pd.DataFrame({ customer_id: [1, 2, 3, 4], name: [张三, 李四, 王五, 赵六], city: [北京, 上海, 广州, 深圳] })这几个方向的差异用一句话总结就是以哪张表为基准就保留哪张表的全部行。inner两边键都匹配上的行才保留匹配不上的直接丢弃。left左表的行全部保留右表匹配不上的地方填NaN。right右表的行全部保留左表匹配不上的地方填NaN。outer两边都保留匹配不上的地方填NaN。拿上面的订单和客户数据来说客户4没有任何订单那么inner合并后客户4不会出现left合并后所有订单都在但客户4也不在因为它在右表outer合并后客户4会出现但它的订单相关字段全是NaN。为实际业务选择how的一个实用建议如果做报表、订单分析通常用left因为你关心的是订单主表的信息完整度客户表缺了就缺了订单不能丢如果要做客户全量分析比如统计每个客户的订单数包括那些一次都没买过的客户就得用outer或right否则那些“沉默客户”会被inner直接过滤掉指标就虚高了。1.3 小心多对多理解笛卡尔积新手最常炸的地方就是键值重复。假设左表里key为a的行有两行右表里key为a的行也有两行inner合并后会发生什么答案是生成4行——每一对都组合一次。这个现象叫笛卡尔积在数据库里是基础常识但Pandas新手往往会吓一跳。left pd.DataFrame({key: [a, a, b], L: [1, 2, 3]}) right pd.DataFrame({key: [a, a, c], R: [10, 20, 30]}) print(left.merge(right, onkey, howinner))结果里a键下会有4行L列的值和R列的值两两组合。这不是Pandas的Bug而是关系代数的标准行为。但它的危险性在于如果你用这种结果去做求和、计数数字会被重复计算导致指标全面失真。2. merge核心参数用对参数就不翻车2.1 on、left_on、right_on指定连接键的三种姿势最简单的场景是左右两边的键列名完全一致这时候直接用onorders.merge(customers, oncustomer_id, howleft)但现实业务里两边列名经常不一样。比如订单表里叫cust_id客户表里叫customer_id这时候on就不够用了得用left_on和right_on分开指定orders pd.DataFrame({ order_id: [1001, 1002], cust_id: [1, 2], amount: [199, 299] }) customers pd.DataFrame({ customer_id: [1, 2], name: [张三, 李四] }) result orders.merge(customers, left_oncust_id, right_oncustomer_id, howleft) print(result)注意这种情况有个小坑合并完的DataFrame里会同时保留cust_id和customer_id两列因为它们名字不一样Pandas不会自动帮你合并成一列。如果你不想要其中一列记得用drop清理。我自己的习惯是合并前先做一次rename把键列统一成同一个名字这样既能用简洁的on又能避免输出表里出现冗余键列。还有一点值得提on参数也可以传一个列表实现多列同时匹配。比如按(年月, 客户ID)两个字段一起连接这在处理流水数据时很常用。2.2 suffixes重复列名怎么办两张表合在一起最怕的就是两边有同名的非键列。比如订单表里有个amount表示订单金额客户表里也有个amount表示客户累计消费额。如果直接合并Pandas会默认给它们加上_x和_y后缀生成amount_x和amount_y。这个默认行为在快速验证时挺方便但放到正式分析里amount_x这种名字含义不明容易误导人。你自己可以通过suffixes参数修改后缀让列名更可读left pd.DataFrame({key: [a], amount: [100]}) right pd.DataFrame({key: [a], amount: [200]}) merged left.merge(right, onkey, suffixes(_订单额, _消费额)) print(merged.columns) # Index([key, amount_订单额, amount_消费额], dtypeobject)给后缀起有业务含义的名字比用默认的_x、_y视频里看着专业多了。另外如果两张表有大量同名但不打算保留的列更推荐的做法是merge之前先把不用的列drop掉从根源上减少冲突。2.3 indicator和validate两个常被忽略的“保险丝”indicatorTrue会在结果里加一列_merge标记每行来自左表、右表还是两表都有。调试的时候这个参数几乎是无价的merged orders.merge(customers, oncustomer_id, howouter, indicatorTrue) print(merged[_merge].value_counts())运行结果会告诉你有多少行是两边都匹配上的有多少行只在左表有多少只在右表。这比你自己肉眼数NaN快得多尤其是处理几万行数据的时候。validate参数则是防止“意外多对多”的保险丝。你可以在合并前提前声明期望的关系比如validateone_to_one、one_to_many、many_to_one。如果实际数据不满足这个约束Pandas会直接抛异常。举个例子orders.merge(customers, oncustomer_id, validatemany_to_one)如果左表的customer_id有重复比如同一个人下了两单这个操作就会报MergeError提醒你数据里有重复键。这个参数在新手期可能用不上但在写生产级数据处理代码时能帮你拦下大量隐藏的数据质量Bug。3. join方法索引上的连接3.1 join和merge到底差在哪很多初学者把join和merge当成两个可以随便互换的东西其实它们最核心的区别只有一个merge默认按列匹配join默认按索引匹配。所谓索引就是DataFrame左侧那组行标签默认是0, 1, 2...但也可以是你自己指定的日期、股票代码、客户ID等。我在实际工作中发现很多人在concat和merge之间反复横跳的时候经常忽略这个问题如果你的两张表索引是有意义的比如日期那join写起来比merge省事得多。但如果你只是想按普通字段连接那直接用merge就好不需要绕道索引。两张表索引对齐的典型场景基金每日净值表和沪深300指数每日行情表。两张表都有日期索引但列不一样你想把同一天的净值变化和市场涨跌幅拼到一行fund pd.DataFrame({ 基金涨跌幅: [0.01, -0.02, 0.03, 0.005], }, indexpd.date_range(2024-04-01, periods4)) market pd.DataFrame({ 沪深300涨跌幅: [0.005, -0.01, 0.02, 0.015], }, indexpd.date_range(2024-04-01, periods4)) result fund.join(market) print(result)这个场景用merge也能做但得先把索引转成普通列再指定on最后再把键列设回索引绕一大圈。join一行就完成了因为两张表的索引天然就是同一天的日期。3.2 用join做索引对齐的典型场景除了日期序列join还特别适合处理以ID为索引的特征表。比如你已经把客户ID设成了索引有一堆后续算出来的特征表也都以客户ID为索引这时join(howleft)就是最自然的选择。还有一种特殊用法是df1.join(df2, onkey)意思是左表按key列去右表右表的索引正好是key里找对应关系。这相当于“按列连接右表的索引”df1 pd.DataFrame({ key: [a, b, c], value: [1, 2, 3] }) df2 pd.DataFrame({ amount: [100, 200, 300] }, index[a, b, c]) result df1.join(df2, onkey) print(result)这个操作其实就是merge的另一种表达但如果你手头的数据已经是“右表以键为索引”的结构用join明显更顺手。记住一个判断标准如果右表索引就是你要匹配的键优先考虑join省去先reset_index再merge的麻烦。4. 实战三张业务表合并的完整流程4.1 场景设定与数据准备理论讲再多不如跑一个完整的例子。假设我有一个迷你的电商数据集三张表分别是订单明细、客户信息、产品信息。订单表里有customer_id和product_id两张外键分别指向客户表和产品表。目标是把三张表合并成一张宽表让每一行订单都带上客户姓名、城市和产品名称、分类。这种“多表拼接”在关系型数据库里是基本功在Pandas里就是连续两次merge。先造数据orders pd.DataFrame({ order_id: [2001, 2002, 2003, 2004, 2005], customer_id: [101, 102, 101, 103, 104], product_id: [P001, P002, P003, P002, P001], order_date: [2025-01-01, 2025-01-01, 2025-01-02, 2025-01-02, 2025-01-03], quantity: [2, 1, 5, 3, 1], price: [29.9, 99.0, 19.9, 99.0, 29.9] }) customers pd.DataFrame({ customer_id: [101, 102, 103, 104], customer_name: [小明, 小红, 小刚, 小丽], city: [上海, 北京, 广州, 深圳], signup_date: [2024-03-01, 2024-05-12, 2024-08-30, 2024-11-11] }) products pd.DataFrame({ product_id: [P001, P002, P003], product_name: [手机壳, 蓝牙耳机, 数据线], category: [配件, 数码, 配件] })4.2 从Excel读取并检查数据实际工作里这些表大概率不会安安静静躺在内存里而是散布在Excel或者数据库里。如果是Excel读取方式是这样的orders pd.read_excel(orders.xlsx, sheet_name订单) customers pd.read_excel(customers.xlsx, sheet_name客户)读完先别急着合并花十秒钟做个体检orders.info()看各列类型和缺失值orders.head()看前五行orders[customer_id].dtype检查键列类型。这一步能省掉后面大量排查时间。尤其是键列我在第5部分会专门讲一个因类型不一致导致合并失败的真实案例几乎每个人都踩过。4.3 分步合并与中间检查第一次合并订单表和客户表按customer_id左连接。为什么用left因为订单是主体我一张订单都不能丢。step1 orders.merge(customers, oncustomer_id, howleft, validatemany_to_one) print(step1.head())加上validatemany_to_one的意思是我确认左边订单表的customer_id是可以重复的一个人多单右边客户表的customer_id必须唯一。如果客户表里真有重复ID这里就会直接报错提醒你数据有问题。合并完之后建议立刻检查行数有没有变化assert len(step1) len(orders), 合并后行数异常这个assert是我个人习惯数据处理的中间步骤但凡能加一个断言后面出问题时就能迅速定位是哪一步出的问题。第二次合并把上一步的结果再和产品表按product_id左连接final step1.merge(products, onproduct_id, howleft, validatemany_to_one) print(final.columns)合并完做一个总检查看每列缺失值情况和总行数print(final.shape) print(final.isna().sum())如果一切正常此时数据应该是一个五行的宽表每个订单都有对应的客户信息和产品信息。4.4 合并后的数据清洗合并只是第一步宽表到手之后通常还伴随着清洗和加工。这里有几个高频操作顺手做了先算订单总金额final[total_amount] final[quantity] * final[price]再把日期字符串转成真正的日期类型final[order_date] pd.to_datetime(final[order_date]) final[signup_date] pd.to_datetime(final[signup_date])日期类型转换的意义在于转完之后你才能做月份提取、时间差计算这类操作。比如我想算“客户注册到首订单之间隔了几天”两个日期类型相减就能得到Timedelta。如果想按城市看销售额直接groupbycity_sales final.groupby(city)[total_amount].sum().sort_values(ascendingFalse) print(city_sales)到这里一个“读取多表 - 按外键合并 - 清洗加工 - 聚合分析”的完整链路就通了。这套流程我几乎每周都会跑几遍区别只是表多了、数据大了核心思路完全一样。5. 常见坑与排查技巧实录5.1 键类型不一致看着该匹配却全是NaN这是我见过最多、也最隐蔽的坑。左边表的customer_id是整数类型int64右边表的customer_id读进来之后变成了字符串object表面上都是数字但底层一个是数值一个是文本merge匹配时永远对不上。left pd.DataFrame({id: [1, 2, 3]}) right pd.DataFrame({id: [1, 2]}) result left.merge(right, onid, howleft, indicatorTrue) print(result[_merge].value_counts()) # 你会发现几乎全是left_only排查方法很简单合并前敲一句print(left[id].dtype, right[id].dtype)看到类型不一致就直接转left[id] left[id].astype(str) # 或者统一转int right[id] right[id].astype(int)转的时候要留意有没有无法转换的值比如空字符串、全角数字这些会导致astype直接抛错或者转到一半失败。稳妥的做法是先dropna再用pd.to_numeric配合errorscoerce兜底。5.2 重复键让你一夜回到笛卡尔积另一个高频事故是重复键导致的行数暴涨。假设客户表里的customer_id不唯一同一个客户注册了两次系统生成了两条记录。订单表和这种客户表做merge时每一张订单都会匹配到两条客户记录订单瞬间翻倍。最直接的表现就是合并后行数突然大于左表聚合求和时金额全部虚高。应对方法分两步第一步在合并前先探查键是否唯一print(customers[customer_id].duplicated().sum())有重复就看你想要哪条记录按业务规则去重customers customers.drop_duplicates(subsetcustomer_id, keeplast)第二步用validate参数兜底像我前面说的那样在merge时声明关系一旦数据不合预期立刻报错而不是悄悄给你膨胀数据。这个小习惯能救命的。5.3 列名冲突suffixes不是万能的merge时遇到重复列名Pandas会自动加后缀这确实方便但连续多次合并会产生非常丑陋的列名——第一次合并后出现amount_x和amount_y第二次合并再撞一次就会变成amount_x_x、amount_x_y这种套娃列名。等到后面写分析代码时你会被这些列名折磨疯。我的建议是尽量减少“带着同名无用列去做merge”的情况。合并前先想清楚这次到底需要哪些列把不用的先drop掉。如果确实要保留两边的同名列就在merge时通过suffixes给它们起有业务含义的名字比如(_订单, _客户)。另外merge之后养成看一眼result.columns的习惯花两秒钟确认列名是否合理比写完几十行代码再来回改高效得多。5.4 join之后索引起乱join的坑主要集中在索引本身就乱的数据上。如果你的左表索引本来就带着重复值或者右表索引没有排序join出来的结果可能出乎意料尤其是在后续做reset_index或者set_index的时候数据顺序可能已经完全不是你想的样子。我建议的原则是在join前先想想索引是不是唯一的、是不是有业务含义。如果索引只是个默认的自增序号那你用join和图方便很可能是在给自己埋坑这种场景下直接merge按列匹配就好。真要在join之后做行筛选或者合并回其他表先reset_index(dropTrue)让数据回到干净的无索引状态避免索引脏数据影响后续操作。6. 性能优化与工程化考量6.1 大厂不建议多表join和Pandas有什么关系网上经常能看到“为什么大厂不建议使用多表join”这种讨论很多人会疑惑那Pandas里也别用merge了吗这完全是两码事。大厂讨论的join通常指数据库/数仓里多张超大表做关联查询因为分布式计算中join意味着数据Shuffle、网络传输、磁盘IO一旦关联的键分布不均还可能引发数据倾斜成本极高。所以业界更倾向于提前把数据加工成宽表查询时单表扫描就完了。但Pandas的merge是内存操作数据量只要在你的机器内存撑得住范围以内合并的性能是可控的完全不需要顾虑所谓“大厂规范”。真正需要警惕的反而是在Pandas里无脑多少次大表merge尤其是在循环里反复合并那才会让性能雪崩。判断标准很简单如果你处理的是几万到几百万行的DataFramemerge随便用如果你的数据已经大到需要分布式计算那就不应该再纠结Pandas里的merge写法了应该去思考数据建模和ETL分层了。6.2 大表merge的五个优化习惯虽然Pandas是内存计算但数据量上来之后优化习惯还是能拉开很大差距的。第一列下推。合并前只保留真正需要的列别一上来就把几百列的表全拼进去。列越少内存占用越小合并时复制和比较的开销也越小。第二键去重。确认键列没有重复值重复键不仅会导致结果行数膨胀还会让合并时的计算量成倍增加。这个在前面已经强调过多对多是性能杀手。第三统一数据类型。数字类型比字符串类型的比较和哈希快得多。如果键列能用整数绝不用字符串能用category类型绝不用object类型。一个category类型的列在合并时性能会有肉眼可见的提升。第四先过滤再合并。如果你只需要某段时间、某类用户的数据先筛选后合并让参与合并的数据量尽量小。这个逻辑和数据库里“先过滤再join”是一个道理。第五避免循环合并。要在循环里不断给一个DataFrame拼新列时每次merge都会产生一个完整的副本。更高效的做法是把所有待合并的表先放到一个列表里然后用reduce或连续的merge一次性处理减少重复复制带来的开销。当然这些优化习惯的前提是先跑通业务逻辑不要一开始就陷入性能调优。数据处理的第一原则永远是“先正确再高效”。逻辑都错了跑得再快也没有意义。最后再分享一点个人体会我刚开始学Pandas的时候总是分不清merge和join到底该用哪个后来想明白一件事——不要死记规则要多想想自己的数据长什么样。列与列之间的业务关系就用merge索引与索引之间的对齐关系就用join。理解了这个章标题里的两个方法你就算真正吃透了。实际工作里我现在的习惯是每次合并前先写一行注释说明“这次为什么要合并、用什么键、期望保留哪些行”这个习惯帮我挡掉了大量低级错误也建议你试试。
返回列表