在数据分析的实战中,我们极少遇到所有数据都干净利落地躺在同一个表格里的情况。更多时候,数据分散在多个文件、多个表,甚至不同的数据源中。如何将这些零散的数据碎片高效、准确地拼接成一个完整的分析对象,是每一位数据从业者必须掌握的核心技能。
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 10
concat将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.0
df1有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数据组合的任督二脉。现在,去你的数据中尝试一下吧!