深度解析Pandas数据组合:从concat到merge,打通你的数据处理任督二脉
在数据分析的实战中,我们极少遇到所有数据都干净利落地躺在同一个表格里的情况。更多时候,数据分散在多个文件、多个表,甚至不同的数据源中。如何将这些零散的数据碎片高效、准确地拼接成一个完整的分析对象,是每一位数据从业者必须掌握的核心技能。
Pandas库为我们提供了两把处理这类问题的“瑞士军刀”:concat(连接)和merge(合并)。它们看似功能重叠,实则各司其职,一个侧重于结构的堆叠,一个侧重于基于键的关联。本文将彻底讲透这两者的区别与联系,并通过丰富的代码示例,让你在实际工作中能游刃有余地应对各种数据组合场景。
一、concat:轴向堆叠的艺术
concat函数的核心思想非常直观:它就像搭积木一样,沿着一条轴(行或列)将多个Pandas对象(Series或DataFrame)简单地堆叠在一起。它不关心数据的“关系”,只关心数据的“位置”。
1. Series与Series的堆叠
先来看最基础的单维数据堆叠。我们创建三个独立的Series,每个都有自己的索引。
import pandas as pd s1 = pd.Series(["A", "B"], index=[1, 2]) s2 = pd.Series(["D", "E"], index=[4, 5]) s3 = pd.Series(["G", "H"], index=[7, 8]) print(pd.concat([s1, s2, s3]))输出:
1 A 2 B 4 D 5 E 7 G 8 H dtype: object默认情况下,axis=0,即按行连接。这相当于把三个Series首尾相接,形成了一个更长的Series。原有的索引被完整保留,这非常有用,因为它保留了每个数据点原始的“身份标签”。
如果我们想把这些Series并排放在一起,形成一个DataFrame,就需要用到axis=1。
print(pd.concat([s1, s2, s3], axis=1))输出:
0 1 2 1 A NaN NaN 2 B NaN NaN 4 NaN D NaN 5 NaN E NaN 7 NaN NaN G 8 NaN NaN H这里发生了两件事:第一,它创建了一个三列的DataFrame,每列对应一个输入的Series;第二,因为各个Series的索引并不完全对齐,Pandas用NaN(Not a Number)自动填充了缺失的位置。这正是concat在列方向拼接时的默认行为——外连接(outer join),即取所有索引的并集。
2. DataFrame与Series的混合堆叠
现实场景中,我们经常需要将一个DataFrame与一个Series进行组合。例如,我们有一个用户基本信息表,现在要追加一行新用户的记录。
df1 = pd.DataFrame(data={"a": [1, 2], "b": [4, 5]}, index=[1, 2]) s1 = pd.Series(data=[7, 10], index=[1, 2], name="a") # 按行连接 print(pd.concat([df1, s1]))输出:
a b 1 1 4.0 2 2 5.0 1 7 NaN 2 10 NaN这里concat将Series s1作为新行添加到了DataFrame df1的下面。请注意,s1拥有自己的索引[1,2],并且它被转换成了一个单列的DataFrame。由于df1有a和b两列,而s1只有a列,因此在b列的位置填充了NaN。
而按列连接时,效果又不同了。
print(pd.concat([df1, s1], axis=1))输出:
a b a 1 1 4 7 2 2 5 10concat将Series s1作为新的一列添加到DataFrame的右侧。有趣的是,df1和s1都包含名为a的列,但concat并不会自动处理列名冲突,而是直接保留,这在后续分析中可能引起混淆,需要我们注意。
3. DataFrame与DataFrame的堆叠
当处理多个结构相似的表时,concat显得尤为高效。
df1 = pd.DataFrame(data={"a": [1, 2], "b": [4, 5]}, index=[1, 2]) df2 = pd.DataFrame(data={"a": [7, 8], "b": [10, 11]}, index=[1, 2]) # 按行连接:追加行 print(pd.concat([df1, df2]))输出:
a b 1 1 4 2 2 5 1 7 10 2 8 11这就像把两个结构完全相同的表格上下叠放在一起。如果索引重复,它也会保留。
# 按列连接:追加列 print(pd.concat([df1, df2], axis=1))输出:
a b a b 1 1 4 7 10 2 2 5 8 11按列连接时,它将两个DataFrame并排放置,形成了更宽的表格。同样,列名重复的问题需要我们自己留意。
4. 重置索引:让数据更规整
很多时候,拼接后的索引可能变得混乱(例如有重复),我们并不关心原始索引,只想得到一个全新的、连续的整数索引。这时,ignore_index=True参数就派上了用场。
pd.concat([df1, df2], ignore_index=True)输出:
a b 0 1 4 1 2 5 2 7 10 3 8 11这个操作非常实用,尤其是在你完成数据清洗、准备将数据导入数据库或进行下一步分析时,一个干净的索引能避免很多麻烦。
5. 像SQL的JOIN一样控制列的交集
concat不仅仅能简单堆叠,它还能通过join参数控制其他轴上的合并逻辑。默认是join='outer'(并集),这意味着所有列都会被保留。
df1 = pd.DataFrame(data={"a": [1, 2], "b": [4, 5]}, index=[1, 2]) df2 = pd.DataFrame(data={"b": [7, 8], "c": [10, 11]}, index=[2, 3]) print(pd.concat([df1, df2]))输出:
a b c 1 1.0 4 NaN 2 2.0 5 NaN 2 NaN 7 10.0 3 NaN 8 11.0df1有a、b两列,df2有b、c两列。join='outer'将三列全部保留,缺失处填充NaN。
如果我们只关心两个DataFrame中都存在的列,可以使用join='inner'(交集)。
print(pd.concat([df1, df2], join="inner"))输出:
b 1 4 2 5 2 7 3 8结果中只保留了公共的b列。这种按列的交集合并,为处理列结构不一致的数据提供了极大的灵活性。
二、merge:基于键的关系型连接
如果说concat是物理上的堆叠,那么merge就是逻辑上的关联。它的工作方式与SQL中的JOIN操作如出一辙,通过一个或多个共同的键(key)将不同DataFrame中的行连接起来。这是数据分析中最常用、最强大的数据组合方式。
1. 理解数据连接的类型
merge的核心是理解参与连接的字段之间的基数关系。这决定了最终结果的行数。
- 一对一连接:最常见的场景,就像员工表和部门表通过唯一的员工ID连接。每个键在左右两边都只出现一次。
df1 = pd.DataFrame({"employee": ["Bob", "Jake", "Lisa", "Sue"], "group": ["Accounting", "Engineering", "Engineering", "HR"]}) df2 = pd.DataFrame({"employee": ["Lisa", "Bob", "Jake", "Sue"], "hire_date": [2004, 2008, 2012, 2014]}) # 默认使用两表中同名的列'employee'进行连接 df_merged = pd.merge(df1, df2) print(df_merged)输出:
employee group hire_date 0 Bob Accounting 2008 1 Jake Engineering 2012 2 Lisa Engineering 2004 3 Sue HR 2014- 多对一连接:例如,员工表(多)和主管表(一)。一个主管管理多个员工,因此“主管”在左表中重复出现,在右表中是唯一的。
df1 = pd.DataFrame({"employee": ["Bob", "Jake", "Lisa", "Sue"], "group": ["Accounting", "Engineering", "Engineering", "HR"]}) df2 = pd.DataFrame({"group": ["Accounting", "Engineering", "HR"], "supervisor": ["Carly", "Guido", "Steve"]}) df_merged = pd.merge(df1, df2) print(df_merged)输出:
employee group supervisor 0 Bob Accounting Carly 1 Jake Engineering Guido 2 Lisa Engineering Guido 3 Sue HR Steve注意观察,Engineering组的主管Guido,被自动匹配给了该组的所有员工Jake和Lisa。
- 多对多连接:当两边的键都存在重复时,就会发生多对多连接,其结果是产生笛卡尔积。例如,员工技能表。一个员工可能有多个技能,一个技能也可能对应多个员工。
df1 = pd.DataFrame({"employee": ["Bob", "Jake", "Lisa", "Sue"], "group": ["Accounting", "Engineering", "Engineering", "HR"]}) df2 = pd.DataFrame({"group": ["Accounting", "Accounting", "Engineering", "Engineering", "HR", "HR"], "skills": ["math", "spreadsheets", "coding", "linux", "spreadsheets", "organization"]}) df_merged = pd.merge(df1, df2) print(df_merged)输出:
employee group skills 0 Bob Accounting math 1 Bob Accounting spreadsheets 2 Jake Engineering coding 3 Jake Engineering linux 4 Lisa Engineering coding 5 Lisa Engineering linux 6 Sue HR spreadsheets 7 Sue HR organization由于左表有2个Engineering,右表也有2个Engineering,因此Engineering组产生了4行结果。
2. 精准控制合并的键
merge提供了多种方式来指定连接键,这是其灵活性的体现。
- 通过on指定共同列名:当两个DataFrame有完全相同名称的连接列时,这是最简洁的写法。
pd.merge(df1, df2, on="employee")- 通过left_on和right_on指定不同的列名:当键的列名不同时,我们需要分别指定。
df1 = pd.DataFrame({"employee": ["Bob", "Jake", "Lisa", "Sue"], "group": ["Accounting", "Engineering", "Engineering", "HR"]}) df2 = pd.DataFrame({"name": ["Bob", "Jake", "Lisa", "Sue"], "salary": [70000, 80000, 120000, 90000]}) pd.merge(df1, df2, left_on="employee", right_on="name")输出:
employee group name salary 0 Bob Accounting Bob 70000 1 Jake Engineering Jake 80000 2 Lisa Engineering Lisa 120000 3 Sue HR Sue 90000- 通过left_index和right_index合并索引:这是一种非常高效的合并方式,尤其当DataFrame的索引本身就代表有意义的实体时。
# 将'employee'列设为索引 df1_indexed = df1.set_index("employee") df2_indexed = df2.set_index("employee") # 通过索引合并 pd.merge(df1_indexed, df2_indexed, left_index=True, right_index=True)此时,结果中的行由索引(employee)决定,而不是列。
3. 驾驭数据连接的集合操作规则(JOIN类型)
这是merge的精髓所在。通过how参数,我们可以精确控制哪些行应该出现在最终结果中。这直接对应了SQL中的四种主要JOIN类型。
- 内连接(inner join,默认):只保留左右两边键都匹配的行。
- 外连接(outer join):保留左右两边键的所有行,不匹配的地方用NaN填充。
- 左连接(left join):保留左表的所有行,右表只保留匹配的行。
- 右连接(right join):保留右表的所有行,左表只保留匹配的行。
df1 = pd.DataFrame({"name": ["Peter", "Paul", "Mary"], "food": ["fish", "beans", "bread"]}) df2 = pd.DataFrame({"name": ["Mary", "Joseph"], "drink": ["wine", "beer"]}) print("内连接:\n", pd.merge(df1, df2, how="inner")) print("\n左连接:\n", pd.merge(df1, df2, how="left")) print("\n右连接:\n", pd.merge(df1, df2, how="right")) print("\n外连接:\n", pd.merge(df1, df2, how="outer"))输出:
内连接: name food drink 0 Mary bread wine 左连接: name food drink 0 Peter fish NaN 1 Paul beans NaN 2 Mary bread wine 右连接: name food drink 0 Mary bread wine 1 Joseph NaN beer 外连接: name food drink 0 Mary bread wine 1 Joseph NaN beer 2 Paul beans NaN 3 Peter fish NaN理解并熟练运用这四种连接方式,是解决复杂数据整合问题的关键。
4. 优雅处理重复列名
当两个DataFrame拥有同名的非连接列时,merge会自动为它们添加后缀_x和_y以作区分。我们可以通过suffixes参数自定义这些后缀,让结果表更具可读性。
df1 = pd.DataFrame({"name": ["Bob", "Jake", "Lisa", "Sue"], "rank": [1, 2, 3, 4]}) df2 = pd.DataFrame({"name": ["Bob", "Jake", "Lisa", "Sue"], "rank": [3, 1, 4, 2]}) print(pd.merge(df1, df2, on="name", suffixes=("_first", "_second")))输出:
name rank_first rank_second 0 Bob 1 3 1 Jake 2 1 2 Lisa 3 4 3 Sue 4 2总结
concat和merge是Pandas数据组合的双子星,它们的设计哲学完美体现了Pandas的简洁与强大。
- concat是“堆叠器”:它专注于数据的物理拼接,无论是行方向的追加,还是列方向的扩展。当你的目标是合并两个结构完全相同、只是数据不同的表,或者想为现有数据简单地添加一行/一列时,concat是最直接、最轻量的选择。
- merge是“关联器”:它模拟了SQL的JOIN操作,是解决复杂数据关联问题的终极武器。当你的数据分散在多个表,且这些表之间通过某种“键”(如用户ID、订单号、日期)存在逻辑关系时,merge就是你首选的工具。它不仅能处理多种基数关系(1:1, 1:M, M:N),还能通过how参数灵活控制连接的逻辑,并通过suffixes等参数优雅地管理列名冲突。
在实战中,一个成熟的Pandas工作流通常会这样组合运用它们:
- 先用read_csv等函数加载多个数据源。
- 用merge根据业务键(如用户ID)将相关的维表(如用户信息表)与事实表(如订单表)进行关联,构建一个宽表。
- 再用concat将这个宽表与按时间或批次新生成的同类数据堆叠在一起,形成完整的分析数据集。
掌握这两个函数,意味着你已经拥有了处理大部分数据组合场景的能力。希望这篇文章能帮助你从原理到实践,彻底打通Pandas数据组合的任督二脉。现在,去你的数据中尝试一下吧!
