Pandas多表操作:合并、连接与拼接的核心区别与实战

发布时间:2026/10/15 20:59:25
Pandas多表操作:合并、连接与拼接的核心区别与实战 1. 为什么“合表”是数据处理的分水岭刚接触表格数据处理那会儿我总觉得把几张表拼在一起是最没技术含量的活儿——不就是把数据摞起来或者按某个字段对上去吗直到有一次帮朋友处理一份销售流水三张表分别来自订单系统、商品档案和门店信息字段命名五花八门主键格式还不统一我硬是用循环加字典匹配写了八十多行代码跑一次要等十几秒改一个逻辑就得重来。后来被一位做数据分析的老哥点醒这类活儿在表格处理库里就是一两行的事问题在于我根本没搞懂合并、连接、拼接这三件事的本质区别。这篇内容就是把我踩过的坑和后来总结出的门道一次性讲清楚。它适合已经会用表格处理库做基本筛选、分组但一遇到多表操作就发怵的人也适合那些能跑通代码但说不清merge和concat到底差在哪的人。核心就三件事纵向拼接把结构相同的表摞起来、横向连接按某个共同字段把两张表并在一起、合并聚合连接的同时做汇总计算。这三件事覆盖了日常数据处理里八成以上的多表场景剩下的两成基本是它们的变体。我见过太多人把concat和merge混着用结果要么行数莫名其妙翻倍要么关键字段全变成空值。根子在于没理解它们各自解决的是什么问题。下面我按“先想清楚要什么再选工具”的顺序把每个操作的适用场景、参数细节和避坑经验拆开讲。你跟着走一遍以后拿到任何多表任务脑子里应该能立刻浮现出该用哪个函数、传哪些参数。2. 纵向拼接把结构相同的表摞起来2.1 什么情况下该用拼接而不是连接纵向拼接解决的是“同一类数据分散在多个表里”的问题。比如每个月的销售记录单独存一张表字段完全一样你要做季度分析就得先把它们摞成一张。这时候用连接按字段匹配是错的因为表之间没有需要匹配的主键关系它们就是同一张表的不同片段。判断标准很简单如果两张表的列名和含义基本一致只是行不同那就是拼接场景。反过来如果两张表描述的是不同维度的信息一张是订单、一张是商品需要通过某个共同字段关联那就是连接场景。我刚开始做数据合并时经常搞反后来养成一个习惯先看列再看行。列相同看行数行要合并就是拼接列不同找共同列按共同列对齐就是连接。拼接的核心参数其实就几个axis决定方向0是纵向、1是横向ignore_index决定是否重置行索引join决定列不一致时取交集还是并集。这几个参数组合起来能覆盖绝大多数场景但每个都有坑下面逐个说。2.2 纵向拼接的实操与参数详解先看最基本的纵向拼接。假设有两张结构相同的表import pandas as pd df_jan pd.DataFrame({ 订单号: [A001, A002, A003], 商品: [键盘, 鼠标, 显示器], 金额: [199, 89, 1299] }) df_feb pd.DataFrame({ 订单号: [B001, B002], 商品: [键盘, 耳机], 金额: [199, 259] }) result pd.concat([df_jan, df_feb], axis0, ignore_indexTrue) print(result)输出是五行数据索引从0到4连续排列。这里ignore_indexTrue是关键——如果不加这个参数结果的行索引会是[0,1,2,0,1]因为两张表各自的索引被原样保留。这种重复索引在后续做筛选或分组时会造成很大困扰比如df.loc[0]会返回两行。所以只要拼接后不需要保留原始索引信息就果断加上ignore_indexTrue。axis参数默认就是0纵向所以纵向拼接时可以不写。但如果你要横向拼接把两张表的列并排放在一起就得显式写axis1。横向拼接有个前提两张表的行数必须一致否则多出来的行会自动填充空值。我一般不建议用横向拼接来合并特征因为一旦行顺序对不上数据就全错位了而且这种错误不会报错只会静默产生错误结果。真要横向合并用merge按索引连接更安全。2.3 列不一致时的处理策略实际工作中更常见的情况是两张表大部分列相同但各有几列是对方没有的。比如一月的表多了“优惠券”列二月的表多了“配送方式”列。这时候concat默认取并集joinouter缺失的位置填NaN。df_jan[优惠券] [YES, NO, NO] df_feb[配送方式] [快递, 自提] result pd.concat([df_jan, df_feb], ignore_indexTrue)结果里一月的“配送方式”是NaN二月的“优惠券”是NaN。这通常是我们想要的——保留所有信息。但如果你明确知道某些列不该出现或者只想保留两张表共有的列就设joininner这样只保留交集列行数不变但列变少。注意join参数控制的是列的对齐方式不是行的。很多人误以为joininner会只保留两张表都有的行实际上行永远是全部保留的纵向拼接时变的只是列。还有一个容易忽略的点拼接时列的顺序按第一张表的列顺序为准后面表里多出来的列会追加在末尾。如果两张表列顺序不同但列名相同concat会自动按列名对齐不会错位。这一点比很多人想象的要智能但前提是列名必须完全一致——差一个空格、一个大小写都算不同列。2.4 拼接时的数据类型陷阱我踩过最深的坑是数据类型不一致导致的拼接问题。比如一张表的“金额”列是整数另一张因为有空值变成了浮点数拼接后整列变成浮点原本的199变成199.0。这在后续做精确匹配或导出时可能引发问题。更隐蔽的是日期类型。一张表的日期是字符串2024-01-15另一张是datetime对象拼接后整列变成object类型排序和筛选都会出问题。我的做法是在拼接之前先统一关键列的数据类型用astype强制转换df_jan[金额] df_jan[金额].astype(float64) df_feb[金额] df_feb[金额].astype(float64)对于日期列统一用pd.to_datetime转换后再拼接。这个预处理步骤多花三十秒能省掉后面半小时的排查时间。3. 横向连接按共同字段把两张表并起来3.1 连接的本质找共同列做匹配连接merge解决的是“不同维度的信息需要关联”的问题。订单表里有商品编号但没有商品名称和单价商品档案表里有编号对应的名称和单价你需要把这两张表按“商品编号”这个共同字段拼在一起让每一行订单都能看到对应的商品信息。这个操作在关系型数据库里叫JOIN在表格处理库里叫merge。核心逻辑是指定一个或多个键列在两张表中找到键值相等的行然后把它们的列合并到同一行。听起来简单但“找不到匹配怎么办”“匹配到多条怎么办”“键列名字不一样怎么办”这三个问题衍生出了所有连接参数。我习惯把连接想象成相亲配对左表是男方名单右表是女方名单键列就是双方的身份证号。身份证号对上了就配成一对配不上的看你怎么处理——是保留单身左连接、还是只保留配对成功的内连接、还是双方都保留外连接。3.2 四种连接方式的区别与选择merge的how参数有四个取值对应四种连接逻辑。我用一个具体例子说明df_orders pd.DataFrame({ 订单号: [A001, A002, A003, A004], 商品编号: [P01, P02, P03, P05] }) df_products pd.DataFrame({ 商品编号: [P01, P02, P03, P04], 商品名: [键盘, 鼠标, 显示器, 耳机], 单价: [199, 89, 1299, 259] })订单表里有P05这个编号但商品档案里没有商品档案里有P04但订单里没出现。四种连接方式的结果差异如下连接方式how取值保留的行本例结果内连接inner只保留键匹配成功的行3行P01/P02/P03左连接left保留左表所有行右表匹配不上的填空4行P05的商品名为空右连接right保留右表所有行左表匹配不上的填空4行P04的订单号为空外连接outer保留两张表所有行匹配不上的互相填空5行P04和P05都在日常用得最多的是左连接。因为通常左表是主数据比如订单右表是补充信息比如商品档案你不想因为右表缺了某条记录就丢掉订单。内连接适合做“只保留双方都有的数据”的过滤外连接适合做数据核对——看看两张表哪些记录对不上。提示merge默认是内连接howinner。如果你不写how参数匹配不上的行会直接消失而且不会报错。这是新手最容易踩的坑之一——明明左表有1000行连接完只剩800行还以为是数据有问题其实是默认内连接把200行没匹配上的丢掉了。3.3 键列名不同时的处理left_on与right_on两张表的键列名字不一样是常态。订单表里叫“商品编号”商品档案里叫“产品ID”这时候用left_on和right_on分别指定df_products_renamed df_products.rename(columns{商品编号: 产品ID}) result pd.merge( df_orders, df_products_renamed, left_on商品编号, right_on产品ID, howleft )这样连接没问题但结果里会同时出现“商品编号”和“产品ID”两列内容完全一样。如果不想保留冗余列连接后用drop删掉一列即可result result.drop(columns[产品ID])另一个更省事的做法是连接前先把列名统一用rename改好再连接这样结果更干净。我一般倾向于后者因为改列名是一次性操作而连接后删列容易忘。3.4 多对多连接的笛卡尔积风险这是连接操作里最危险的情况。如果左表的键值在右表中对应多条记录连接结果会按匹配数量成倍膨胀。比如左表一个订单对应商品编号P01右表里P01有三条记录不同供应商连接后这个订单会变成三行。df_products_multi pd.DataFrame({ 商品编号: [P01, P01, P02], 供应商: [供应商甲, 供应商乙, 供应商丙] }) result pd.merge(df_orders, df_products_multi, on商品编号, howleft)原本4行的订单表连接后变成5行P01出现两次各对应一个供应商。如果右表匹配更多膨胀更严重。做连接前一定要检查右表的键列是否唯一print(df_products_multi[商品编号].duplicated().any())如果返回True说明有重复键连接前要想清楚是取第一条、做聚合还是其他处理。我通常的做法是先用groupby把右表按关键字段聚合到唯一再连接这样结果可控。3.5 连接后的索引与排序问题merge默认会重置索引结果的行索引是新的连续整数。这本身没问题但如果你后续要用loc按索引取数就得注意索引已经变了。另外连接后的行顺序默认是按左表顺序排列的但匹配到多条时会插入在对应位置整体顺序可能和你预期不同。如果需要保持某种顺序连接后显式排序result result.sort_values(订单号).reset_index(dropTrue)reset_index(dropTrue)把排序后的索引重置为连续整数避免索引乱序影响后续操作。这个习惯我建议养成——每次连接或拼接后都重置一次索引能避免很多“为什么iloc[0]取到的不是第一行”的困惑。4. 合并聚合连接的同时做汇总计算4.1 什么时候需要“连接聚合”一步到位有些场景下你不需要把明细数据全部连接过来而是只需要汇总值。比如订单表里每个订单有商品编号你想知道每个订单对应的商品所属品类的平均单价而不是每个商品的单价明细。这时候如果先连接再分组聚合中间会产生大量冗余行效率低且容易出错。更优雅的做法是先在右表做聚合再把聚合结果连接到左表。这样右表在连接前就已经是每个键一行不会产生多对多膨胀。df_products[品类] [外设, 外设, 显示设备, 音频] category_avg df_products.groupby(品类)[单价].mean().reset_index() category_avg.columns [品类, 品类均价] df_orders_with_category pd.merge( df_orders, df_products[[商品编号, 品类]], on商品编号, howleft ) result pd.merge( df_orders_with_category, category_avg, on品类, howleft )这个两步连接看起来多了一步但每一步的右表都是唯一的不会产生行数膨胀。而且聚合后的数据量小连接速度快。4.2 用groupbyagg做多维度聚合如果右表需要按多个维度聚合groupby配合agg可以一次性算出多个统计量product_stats df_products.groupby(品类).agg( 商品数量(商品编号, count), 平均单价(单价, mean), 最高单价(单价, max), 最低单价(单价, min) ).reset_index()这样得到的product_stats每个品类一行包含四个统计列。连接到订单表后每个订单都能看到对应品类的统计信息。agg里用字典形式指定“新列名(原列名, 聚合函数)”的写法比先聚合再改列名清晰得多。注意groupby之后如果直接连接索引是品类名而不是列。必须加reset_index()把索引变回普通列否则merge时找不到“品类”这个键列。这个坑我踩过不止一次每次都是连接结果全空才想起来。4.3 连接键存在空值时的处理如果连接键列里有空值NaNmerge默认不会把空值和空值匹配在一起。也就是说左表键为NaN的行在右表里即使也有NaN也不会匹配成功结果中右表的列全是空。df_left pd.DataFrame({key: [A, None, C], val: [1, 2, 3]}) df_right pd.DataFrame({key: [A, None, D], info: [x, y, z]}) result pd.merge(df_left, df_right, onkey, howleft)结果中key为None的那行info列是NaN而不是y。这是设计如此——空值代表“未知”两个未知值不相等。如果确实需要让空值匹配得先填充一个特殊标记df_left[key] df_left[key].fillna(__MISSING__) df_right[key] df_right[key].fillna(__MISSING__)连接后再把标记改回空值。这个技巧在处理真实数据时很有用因为真实数据里空值匹配空值往往是合理的业务需求。4.4 聚合结果的验证与对账连接聚合完成后一定要做行数验证。我习惯在连接前后各打印一次行数print(f连接前左表行数: {len(df_orders)}) print(f连接后行数: {len(result)})如果连接后行数变多说明右表有重复键导致膨胀如果变少说明用了内连接且有关键匹配不上。两种情况都需要排查。对于左连接行数应该和左表完全一致除非右表有重复键。这个简单的检查能拦住大部分连接错误。另外对于聚合列可以抽查几行手工验证。比如取一个订单找到它的商品编号去商品表里查对应品类的平均单价和连接结果对比。这种手工验证做两三次就能确认逻辑是否正确。5. 常见问题与排查技巧实录5.1 连接后数据变多或变少怎么办这是最高频的问题。行数变多的原因几乎总是右表键列有重复导致一对多膨胀。排查方法# 检查右表键列是否有重复 duplicated_keys df_right[df_right[key].duplicated(keepFalse)] print(duplicated_keys.sort_values(key))把重复的键列出来看看是数据本身就该有多条比如一个商品多个供应商还是数据质量问题比如重复录入。如果是前者需要先聚合或取第一条如果是后者需要去重。行数变少的原因通常是用了内连接且左表有关键在右表找不到。排查方法# 找出左表有但右表没有的键 left_keys set(df_left[key]) right_keys set(df_right[key]) missing left_keys - right_keys print(f左表有但右表缺失的键: {missing})找到缺失的键后要么补充右表数据要么改用左连接保留左表所有行。5.2 键列数据类型不一致导致匹配失败这个坑极其隐蔽。左表的键是整数1右表的键是字符串1merge不会报错但匹配结果全空。因为整数和字符串不相等。df_left pd.DataFrame({id: [1, 2, 3], val: [a, b, c]}) df_right pd.DataFrame({id: [1, 2, 4], info: [x, y, z]}) result pd.merge(df_left, df_right, onid, howleft) # 结果中info列全是NaN排查方法连接前打印两张表键列的dtypeprint(df_left[id].dtype) # int64 print(df_right[id].dtype) # object统一类型后再连接df_right[id] df_right[id].astype(int)我现在的习惯是连接前必查键列类型尤其是从不同来源读取的数据比如一个来自CSV、一个来自数据库类型不一致的概率很高。5.3 列名冲突时的自动加后缀两张表有同名列但不是键列时merge会自动加后缀_x和_ydf_left pd.DataFrame({key: [A, B], value: [1, 2]}) df_right pd.DataFrame({key: [A, B], value: [10, 20]}) result pd.merge(df_left, df_right, onkey) # 结果列key, value_x, value_y这个自动后缀有时候方便有时候烦人。可以用suffixes参数自定义result pd.merge(df_left, df_right, onkey, suffixes(_订单, _商品))这样列名变成value_订单和value_商品更易读。如果同名列其实含义相同应该在连接前就重命名或删除避免结果里出现两列相似数据。5.4 拼接时列顺序不一致的处理concat按列名对齐所以列顺序不一致不会导致数据错位。但结果里的列顺序是按第一张表的顺序排列的后面表独有的列追加在末尾。如果你希望结果列顺序统一可以在拼接后重新排列cols [订单号, 商品, 金额, 优惠券, 配送方式] result result[cols]或者拼接前把所有表的列顺序统一df_jan df_jan[cols] df_feb df_feb[cols]后者更稳妥因为拼接前就保证了结构一致。5.5 大数据量下的性能优化当表有几十万行时merge和concat的性能差异会很明显。几个优化点连接前先筛选如果只需要部分行先用条件筛选再连接减少参与连接的数据量。键列设索引merge时如果键列是索引速度会快一些。可以用set_index把键列设为索引再连接。避免不必要的列连接前只保留需要的列减少内存占用和计算量。用category类型如果键列是字符串且重复值多转成category类型能大幅提速。df_orders[商品编号] df_orders[商品编号].astype(category) df_products[商品编号] df_products[商品编号].astype(category)这个优化在百万行级别能带来数倍的性能提升而且代码改动极小。5.6 常见问题速查表问题现象可能原因排查方法解决方案连接后行数变多右表键列有重复df_right[key].duplicated().any()先聚合右表或去重连接后行数变少内连接且有关键匹配不上对比左右表键集合改用左连接或补充数据连接结果全空键列数据类型不一致打印两表键列dtype统一类型后连接拼接后索引重复未设ignore_indexTrue检查结果索引加ignore_indexTrue拼接后出现多余列两表列不一致且取并集对比两表列名设joininner或统一列连接后列名带_x/_y有同名列非键列检查列名用suffixes自定义或提前重命名空值键匹配不上空值不等于空值检查键列是否有NaN填充特殊标记后再连接6. 几个让我少走弯路的实操习惯6.1 连接前先画“表关系图”在纸上或脑子里画一下左表是什么粒度一行代表什么右表是什么粒度键列是什么连接后一行代表什么。这个简单的思考能避免大部分逻辑错误。比如订单表一行是一个订单商品表一行是一个商品按商品编号连接后一行还是一个订单如果商品编号唯一但如果商品表一个商品有多个供应商连接后一行就变成“订单-供应商”组合粒度变了。粒度变化是连接操作最本质的影响。每次连接前问自己连接后一行代表什么如果答案和左表不同就要想清楚这是不是你要的。6.2 用样本数据先跑通再上全量不要一上来就拿全量数据跑。先取左表前100行、右表前100行跑一遍连接检查列名、行数、关键字段值。确认无误后再上全量。这个习惯能让你在几秒内发现问题而不是等几分钟跑完才发现结果不对。sample_left df_orders.head(100) sample_right df_products.head(100) sample_result pd.merge(sample_left, sample_right, on商品编号, howleft) print(sample_result.head(10))样本跑通后把同样的代码套到全量数据上结果基本不会有意外。6.3 保留中间结果便于回溯连接和拼接往往不是一步完成的中间会产生多个DataFrame。我习惯给每个中间结果起有意义的名字而不是反复覆盖同一个变量orders_with_product pd.merge(df_orders, df_products, on商品编号, howleft) orders_with_category pd.merge(orders_with_product, df_category, on品类, howleft) final_result orders_with_category.groupby(品类)[金额].sum()这样如果最后结果不对可以逐步往前检查是哪一步出了问题。如果全部用df覆盖出了问题只能从头重跑。6.4 导出前重置索引并检查列顺序最终结果导出前做两件事reset_index(dropTrue)重置索引按业务需要的顺序排列列。这两步花不了几秒但能让导出的文件干净很多。我见过太多人导出的表格第一列是乱七八糟的索引或者列顺序和需求文档对不上还得手工调整。final_result final_result.reset_index(dropTrue) final_result final_result[[订单号, 商品名, 品类, 金额]] final_result.to_csv(result.csv, indexFalse)indexFalse避免把索引写进文件这个参数几乎每次导出都要加。6.5 关于连接键选择的经验连接键尽量选唯一标识性强的列。订单号、商品编号这类有明确唯一性的字段是首选。避免用“日期”“金额”这类可能重复的列做键除非你明确知道会有多对多匹配并且能处理膨胀。如果实在没有唯一键可以考虑用多列组合作为键pd.merge(df_left, df_right, on[日期, 门店编号, 商品编号], howleft)多列组合能提高唯一性但也会增加连接的计算量。权衡一下如果单列键重复率不高且业务上可接受用单列更简单。另外连接键的命名最好统一。如果左表叫“商品编号”右表叫“产品ID”虽然能用left_on/right_on处理但后续维护时容易混淆。我一般会在数据接入阶段就把关键字段名统一后面所有连接都用on参数代码更干净。6.6 拼接与连接的选择再强调最后再强调一次选择逻辑列相同、行不同用拼接列不同、需要按字段关联用连接。这个判断标准覆盖了绝大多数场景。如果两张表既列不同又行不同那通常是先连接再拼接或者先拼接再连接需要根据业务逻辑拆成两步。我处理过一个场景三个月的销售数据分别在三张表里每张表都需要连接商品档案获取商品名。这时候有两种做法先把三张表拼接成一张大表再连接商品档案或者每张表分别连接后再拼接。前者效率更高连接一次后者逻辑更清晰每月独立处理。我选了前者因为商品档案不大连接一次比分三次快而且拼接后数据量虽然大但连接操作本身不复杂。这个选择没有绝对对错取决于数据量和业务需求。关键是理解每一步在做什么以及结果是否符合预期。多跑几次样本多检查行数和关键字段慢慢就有手感了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询