先别急着背文档,我直接说结论:很多人学 pandas 学到一半,碰到 concat、merge、join 这三个东西就开始晕,因为光看名字都像“合并数据”,但实际用起来完全不是一回事。我最初也被坑过好多次,在项目里写错函数,要么数据行数翻了好几倍,要么直接报 key error,后来把三个函数的定位、参数和底层逻辑彻底撸了一遍,才终于不再纠结。这篇就把我的理解从头到尾拆开讲,包括它们各自适用什么场景、参数怎么选、踩过哪些坑,希望能让你一次搞明白。
1. pandas 数据合并三兄弟:先搞清楚它们各自是干嘛的
1.1 一张表看懂 concat、merge、join 的核心定位
先说整体印象。concat 干的活是“拼接”,它是把两个 DataFrame 按照行方向或列方向直接堆在一起,不关心你有没有共同的键,纯粹是物理叠加;merge 干的活是“关联”,它是像 SQL 里的 JOIN 那样,按照一个或多个公共列把行配对起来;join 则是“偷懒版的 merge”,它默认按照索引来关联,写法更简洁,但能力上基本是 merge 的子集。
用一张表把它们的核心差异列出来,后面再逐个细讲:
| 函数 | 合并依据 | 默认方向 | 典型场景 | 对应 SQL 类比 |
|---|---|---|---|---|
| concat | 无共同字段,纯位置拼接 | axis=0 沿行方向堆叠 | 多天日志、多张结构相同的表合并 | UNION ALL(结构一致时) |
| merge | 指定公共列或多列 | 内连接(交集) | 订单表关联商品表、用户表关联行为表 | JOIN(on 指定字段) |
| join | 默认索引,也可指定列 | 左连接 | 两表索引一致的评分表合并、时间序列对齐 | JOIN ON index |
注意一个容易忽略的点:concat 虽然也能在 axis=1 时按索引对齐,但它本质还是“横向拼接”,数据行位置对不上时会产生 NaN,这跟 merge 的匹配逻辑是完全不同的。
1.2 concat:把数据“摞”起来的拼接操作
concat 的核心是“堆”。假设你有三个 CSV 文件,分别记录周一、周二、周三的订单明细,它们的列名完全一样,只是行数不同。你想把它们变成一张包含三天的总表,这就是 concat 的标准用法。
import pandas as pd df_monday = pd.DataFrame({"订单号": [1, 2], "金额": [10, 20]}) df_tuesday = pd.DataFrame({"订单号": [3, 4], "金额": [30, 40]}) df_all = pd.concat([df_monday, df_tuesday], ignore_index=True) print(df_all)输出就是一个新表,行从上到下依次是周一的 1、2 和周二的 3、4。这里有个关键点:concat 默认是 axis=0,也就是沿着行方向往下摞。如果你把 axis 改成 1,就是把两个 DataFrame 像贴海报一样横向并排放,这在两个表没有共同键、但行顺序恰好有意义时会用到。
为什么用 concat 而不是 merge 来做这种拼接?因为 merge 需要指定一个连接键,如果你没有任何公共列,还要硬用 merge,那就得自己造一列相同的键,非常别扭。concat 不关心内容是否一致,只要结构对得上就行,所以多文件批量读入后第一步操作基本都是 concat。
但 concat 容易踩的坑是:两个表的列名不一致时,行拼接会保留所有列,缺失部分自动填 NaN。如果你的周一表是“订单号”和“金额”,周二表是“订单号”和“支付方式”,拼出来的表就会既有“金额”也有“支付方式”,没数据的行全是 NaN。这个行为有时候是特性,有时候是你数据口径不统一的坑。
1.3 merge:按“公共钥匙”配对的行连接
merge 才是真正意义上的“合并”。它的底层逻辑是关系型数据库里的 JOIN 操作:左手表(left)和右手表(right)各给出一列或多列作为“钥匙”,然后按照钥匙的相等关系把行配对。
举个例子:有一张订单表,里面只有“用户编号”,没有用户姓名;另一张用户表,有“用户编号”和“姓名”。现在要把姓名填到订单表里,这就是 merge 的标准场景。
orders = pd.DataFrame({ "订单号": [101, 102, 103], "用户编号": ["u1", "u2", "u1"] }) users = pd.DataFrame({ "用户编号": ["u1", "u2", "u3"], "姓名": ["张三", "李四", "王五"] }) result = pd.merge(orders, users, on="用户编号", how="left") print(result)输出里订单 101 和 103 都关联到了“张三”,102 关联到“李四”。这就是 merge 的最大价值:它能把两张结构完全不同、只有个别字段重合的表关联起来,得到一张信息更完整的宽表。
merge 的 how 参数决定保留哪些行,默认是 how="inner",只保留两边钥匙能对上的行。实际工作中我更多用 how="left",因为业务上通常要保留左表的所有记录,比如订单表哪怕用户信息缺失,订单也不能丢。后面我会专门讲这几个连接方式的区别。
1.4 join:基于索引的“偷懒版”合并
join 这个方法挂在 DataFrame 上,写法是df1.join(df2)。它和 merge 最大的区别是:默认不指定列,而是用两个 DataFrame 的索引(index)来对齐。
举个例子,你有一份商品维表,索引是“商品 ID”,另一份商品评分表,索引恰好也是“商品 ID”,你想把评分拼到维表后面,这时候直接df_sku.join(df_score)就完事了,不用写 on 参数。
df_sku = pd.DataFrame( {"名称": ["手机", "电脑", "平板"]}, index=["A001", "A002", "A003"] ) df_score = pd.DataFrame( {"评分": [4.5, 4.8, 4.2]}, index=["A001", "A002", "A003"] ) result = df_sku.join(df_score) print(result)你觉得 join 是不是就是 merge 的一个快捷方式?可以这么理解。事实上 join 的底层实现最终也是调用 merge,只不过它默认把左边的索引和右边的索引作为连接键。如果你在 join 里显式指定on="某列",那就是把左表的那一列和右表的索引对齐,这是唯一比默认 merge 顺手的地方。
但 join 也有局限:如果两边的索引没有业务意义,或者你需要多列同时匹配,直接用 join 会有点别扭,这时候老老实实写 merge 反而更清晰。
2. 核心参数拆解:每个函数到底怎么用
2.1 concat 的参数:axis、join、keys、ignore_index
concat 最常用的参数有四个,我先逐个说,再说组合用法。
axis:决定拼接方向。0 是行拼接,1 是列拼接。默认 0。join:决定拼接时如何处理索引。join="outer"是并集,join="inner"是交集。默认 outer。注意这里的 join 和 DataFrame.join 的 join 不是一回事。ignore_index:是否忽略原来的索引重新排列。默认为 False。如果原表索引有重叠,建议设成 True。keys:给拼接后的数据打一个分组标签,常用于区分数据来自哪个表。
你可能会遇到一个场景:多天的数据列顺序不完全一致,比如周一表是“日期、订单号、金额”,周二表是“日期、金额、订单号”。concat 在 axis=0 时会按列名对齐,所以列顺序不一致其实不影响结果,它会把相同列名对应上,这一点很多新手不知道。
再看一个 keys 的例子:
df_week = pd.concat( [df_monday, df_tuesday], keys=["周一", "周二"] ) print(df_week.index)这时候索引是 MultiIndex,第一层是“周一”“周二”,你就能通过df_week.loc["周一"]方便地切出原来那份数据。这在批量处理多个来源文件、后续还要按来源筛选时特别好用。
ignore_index和keys其实有冲突,用了 keys 通常就不 ignore_index,因为 keys 本身会构造一个分层索引。我个人的建议是:批量读文件做汇总时优先用ignore_index=True,因为你大概率不再需要区分来源;如果还要回查来源,那就保留 keys。
2.2 merge 的参数:how、on、left_on/right_on、suffixes
merge 的参数比 concat 多,但真正天天用的其实就四个。
先说how。它决定了连接方式,对应 SQL 里的 join type:
how="inner":只保留两边都能匹配上的行,交集。how="left":保留左表全部行,右表匹配不上的填 NaN。how="right":保留右表全部行,左表匹配不上的填 NaN。how="outer":两边全部保留,匹配不上的填 NaN,并集。
然后是on,这是最直观的方式,两边的列名一致时直接用。比如pd.merge(orders, users, on="用户编号")。但如果两边的列名不一样,比如左表叫“用户编号”,右表叫“user_id”,这时候就要用left_on和right_on分别指定:
pd.merge(orders, users, left_on="用户编号", right_on="user_id", how="left")用 left_on/right_on 的时候有个坑:结果里两列都会保留,一列叫“用户编号”,一列叫“user_id”,而不是自动帮你合并成一列。如果你只想要一个键,可以在 merge 之后手动 drop 掉多余的那列。
最后是suffixes。当两表除了连接键之外,还有同名的其他列时,merge 会自动给列名加后缀区分。默认是_x和_y,你可以通过suffixes=("_左表", "_右表")改成更好懂的名字。这个参数我后面避坑部分会重点讲,因为很多人在这里被列名弄晕。
2.3 join 的参数:how、on、lsuffix/rsuffix,以及它和 merge 的关系
DataFrame.join 的签名是df_left.join(df_right, how="left", lsuffix="", rsuffix=""),但它也有一个 on 参数。这里的 on 意思和 merge 不太一样:当 on 缺省时,它用左表的索引去对齐右表的索引;当 on 指定某个列时,它是用左表的这个列去对齐右表的索引。
lsuffix和rsuffix是专门用来处理列名冲突的。因为 join 默认按索引对齐,不涉及连接键列名的问题,但两表中如果有同名的普通列,冲突时就会报错,除非指定后缀。
df_sku = pd.DataFrame( {"名称": ["手机", "电脑"], "评分": [None, None]}, index=["A001", "A002"] ) df_score = pd.DataFrame( {"评分": [4.5, 4.8]}, index=["A001", "A002"] ) result = df_sku.join(df_score, lsuffix="_维表", rsuffix="_评分") print(result)这样输出里会有“评分_维表”和“评分_评分”两列,你能很清楚地看到哪份数据来自哪张表。
从实现角度说,pandas 的 join 底层就是把索引变成连接键,然后调用 merge:
# 近似等价 pd.merge(df_sku, df_score, left_index=True, right_index=True, how="left")所以如果你已经会 merge,join 就是省去写left_index=True, right_index=True的语法糖。但是 join 在“左表用普通列、右表用索引”这种混合匹配时很方便,merge 则需要分别写left_on和right_index=True,明显更啰嗦。
3. 记忆法 + 实战场景对照
3.1 一句话记忆:轴向拼接用 concat,按键合并用 merge,索引合并用 join
我知道前面讲了一堆参数,记不住很正常。我自己最后总结成了一句话:
- 结构相同,要堆叠:选 concat。
- 有共同字段,要关联:选 merge。
- 有共同索引,要对齐:选 join。
如果你临时判断不出来,就抓着两个问题问自己:我要不要一个“共同的列”来配对?如果要,走 merge;如果只是从上往下摞,走 concat;如果两边索引恰好有业务意义,走 join。
这句话虽然粗糙,但遇到 80% 的场景都够用了。剩下 20% 的情况,就是三个函数之间可以互相转换,比如 concat(axis=1) 也能做索引对齐,merge(left_index=True, right_index=True) 也能做索引连接,但这种写法要么可读性差,要么参数麻烦,除非特殊需求,不然没必要绕。
3.2 实战场景一:多天日志拼接
先说最典型的场景:用 concat。假设你有一个定时任务,每天生成一份用户行为日志文件,文件名带日期,列结构完全一致。数据团队每天要做全量分析,第一步就是把几十个文件读进来拼成一张大表。
import pandas as pd from pathlib import Path files = list(Path("logs").glob("*.csv")) frames = [pd.read_csv(f) for f in files] df_logs = pd.concat(frames, ignore_index=True)这里有个细节:直接 concat 几十个 DataFrame,如果其中某些文件列数不一致,最终表会多出许多 NaN。所以我在真实项目里会先做一次列名校验:
columns = [set(df.columns) for df in frames] common = set.intersection(*columns) if len(common) != max(len(c) for c in columns): print("存在列不一致,需要处理")这个小检查能帮你提前发现脏数据,而不是等到分析时才发现字段对不上。
3.3 实战场景二:订单表关联商品表
如果用 concat 处理订单表和商品表,那完全就是在瞎搞,因为两张表结构不同,一个只有商品 ID,一个还有商品名称和价格,直接把列拼在一起没有任何意义。这时候要用 merge。
真实业务里,订单表可能有百万行,商品表只有几千行。你只需要把商品名称匹配过来:
orders = pd.read_csv("orders.csv") products = pd.read_csv("products.csv") orders_full = pd.merge(orders, products, on="product_id", how="left")用 how="left" 的原因是:订单是业务事实表,每一行都必须保留,商品信息匹配不上是数据质量问题,但不能因为缺失就删掉订单。如果用 inner,那些商品已下架的订单就会被静默丢掉,这在报表里是很危险的。
3.4 实战场景三:索引对齐的评分表合并
最后是 join 的典型场景。我遇到过一份用户基础信息表,索引是用户 ID,另一份是标签表,索引也是用户 ID,只是两者列不同,想合并成一份宽表供后续模型用。
user_info = user_info.join(user_tags, how="left")代码简单到不能再简单。如果换成 merge,你得写两遍 left_index=True、right_index=True,还容易漏。所以当左右两表索引都是同一个业务 ID 时,join 就是最舒服的选择。
但我也要提醒:join 默认 how="left" 这一点很多人没想到。DataFrame.join 默认保留左表全部行,右表只取能匹配上的,So 如果你的右表有左表没有的索引,那部分行不会出现在结果里。如果需要全量并集,要显式 how="outer"。
4. 实战避坑指南:索引、重复列名、笛卡尔积
4.1 索引混乱问题:ignore_index 到底用不用?
concat 最常见的一个坑就是索引没有重置。假设 df_monday 的索引是 0、1,df_tuesday 的索引也是 0、1。直接pd.concat([df_monday, df_tuesday]),得到的新表索引会是 0、1、0、1,不是 0、1、2、3。
这样带来的问题很直接:如果你后续要用布尔索引或者 loc 操作,会莫名其妙地选中多行。比如df_all.loc[0]会同时返回周一和周二的第 0 行,容易误导你。
解决办法有两个:一是 concat 时加ignore_index=True;二是 concat 后重新reset_index(drop=True)。我习惯用前者,因为一次到位。
merge 也有类似的隐患。如果你合并后不重置索引,新表的索引来自左表,可能出现重复。虽然 merge 本身不会重排索引,但后续 apply 或 groupby 时可能会因为索引重复而报一些隐性问题。稳妥的做法是合并完直接reset_index(drop=True)。
4.2 重复列名处理:suffixes 和合并后的列名
merge 后最让我头疼的就是重复列名。比如订单表有“金额”(下单金额),退款表也有“金额”(退款金额),按订单号 merge 之后,你会得到两列都叫“金额”的情况。pandas 默认把它们改成“金额_x”和“金额_y”,如果你没注意,很容易把字段取错。
更麻烦的是,如果两个表的连接键是通过 left_on 和 right_on 指定但列名不同,结果里会出现两个连接键列,比如“用户编号”和“user_id”,处理起来也要细心。
我推荐的做法是:merge 之前先给非连接键字段重命名,让它带上下文。比如把退款表的“金额”先改成“退款金额”,再合并,这样结果列名直接可读,不需要依赖 suffixes。
如果一定要依赖 suffixes,那就要显式传参:
pd.merge(df_order, df_refund, on="order_id", suffixes=("_订单", "_退款"))这样至少结果里是“金额_订单”“金额_退款”,一眼能看懂。千万不要什么都不传,默认的 _x、_y 在数据量大的时候特别容易看走眼。
4.3 警惕笛卡尔积:没有共同键时会发生什么
这是 merge 最危险的地方。当两个 DataFrame 的连接键不是唯一值时,merge 会产生多行匹配,专业点叫笛卡尔积。比如左表有两个相同的“用户编号”行,右表也有两个相同的“用户编号”行,merge 后就会生成 2×2=4 行。
表面上看,这好像是数据变多,其实是重复匹配。这种问题在用户维度多对多关联时特别常见,比如一个用户有多笔订单,另一个表里这个用户有多条标签,直接把两个表 merge 到“用户编号”上,就会把订单和标签的组合全部展开,行数爆炸。
避免笛卡尔积的方法:
- 合并前检查连接键的唯一性:
df.groupby("键").size().max()看是否为 1。 - 如果业务上需要多对多,务必明确最终粒度是什么。是要订单粒度还是标签粒度?如果只是想要用户维度的聚合结果,先 groupby 再 merge。
- 对于确实需要多对多又不想膨胀的场景,考虑先把其中一个表去重,或者按业务规则指定主表。
我在真实项目中遇到过最夸张的一次:两个表各 10 万行,因为连接键重复,合并后变成了 300 万行,程序直接内存溢出。所以现在只要是 merge,我第一步先看连接键的唯一性,不会盲目合并。
5. 常见问题排查与个人建议
5.1 报错和异常速查表
我把自己工作中遇到的典型问题整理成一个速查表,方便你遇到状况时直接对照:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| concat 后出现大量 NaN | 两表列名不一致 | 统一列名后再 concat,或检查数据来源 |
| concat 后索引重复 | 没有用 ignore_index | 加上 ignore_index=True |
| merge 报 ValueError: You are trying to merge on object and int64 columns | 两边键的数据类型不一致 | 用 astype 统一类型,比如都转成 str |
| merge 报 KeyError | 指定的 on 列在某一表中不存在 | 检查列名是否拼写错误,或使用 left_on/right_on |
| merge 后行数远超预期 | 连接键不唯一,产生了多对多 | 先检查键是否唯一,必要时去重 |
| join 报 columns overlap but no suffix specified | 两表存在同名列且没给后缀 | 指定 lsuffix、rsuffix |
| 合并后找不到想要的列 | 默认 suffix 把列名改了 | 提前重命名或显式 suffixes |
其中数据类型不一致这个问题最常见。左表的用户编号读进来是字符串,右表用户编号读进来是整数,merge 时 pandas 不会自动帮你做符号转换,有些版本会直接报错,有些版本会转成 object 硬匹配,结果匹配不上的时候全是 NaN。所以我在 merge 前都会统一检查连接键的 dtype。
5.2 该选哪个:一个简单的决策流程
如果你想快速判断,可以用下面这个流程走一遍:
- 先问自己:两个表的行是不是要“上下摞起来”还是“左右配对”?
- 上下摞 → 直接用 concat,别想 merge。
- 左右配对 → 继续看下一步。
- 左右配对时,你的匹配依据是什么?
- 如果是普通列值相等 → 用 merge,指定 on / left_on / right_on。
- 如果是索引相等 → 用 join,或者 merge(left_index=True, right_index=True)。
- 然后确定 how:
- 保留左表全部 → left。
- 只保留两边都能匹配上 → inner。
- 保留全部字段 → outer。
- 保留右表全部 → right。
- 最后处理结果:
- 是否需要 reset_index。
- 是否有重复列名。
- 是否行数异常。
这套流程走完,基本不会选错。我自己现在写 code review 的时候,看到别人 merge 和 concat 乱用,也都是照着这个思路让他自查。
5.3 我自己的工程习惯和额外提醒
最后分享几个我长期形成的习惯,不一定适合所有人,但至少能帮你减少低级错误:
第一,只要不是刻意要用索引,我会把数据的唯一键尽量做成一列普通列。这样后续 merge 统一用 on 参数,代码可读性最高。纯索引对齐只用在时间序列或者索引本身有业务含义的场景。
第二,合并之前先看一眼两个表的形状和列名。我的一条铁律是:任何合并操作,至少先打印df.shape和df.columns。如果 merge 之后行数跟我预估的不一致,我会立刻停下来追查原因,绝不带着疑问往下走。
第三,能用 left join 解决的事情,默认不轻易用 inner join。因为 inner 会丢行,而这个“丢弃”往往是静默的,你很难发现。left join 至少能让你看到哪些行匹配失败,方便后续排查。
第四,关于性能:concat 在大量小文件拼接时很稳,但如果单个文件非常大,建议先pd.read_csv(..., usecols=需要的列),减少内存占用。merge 两个大 DataFrame 时,尽量把小表放在右边,pandas 对 hash join 的实现会让内存压力更可控。这个是我在实际跑几千万行数据时感受到的差异。
所以你看,concat、merge、join 的区别说穿了就是三种不同维度的合并思路。日常用 pandas,真正高频的无非是 concat 加 ignore_index、merge 加 left 和 on,以及 join 做索引对齐。先把这三个固定组合记熟,遇到更复杂的多列连接、外连接等情况再回去翻参数细节,你会发现整个 pandas 数据合并逻辑一下就通顺了。