跳至主要内容

在 pandas 中连接 DataFrame:merge()、concat() 与 join()

通过实际示例,学习在 pandas 中使用 concat()、merge() 和 join() 来连接与合并 DataFrame。
· 13分钟 阅读

使用 AI 探索

您是否尝试过解 Kaggle 挑战?如果试过,您可能会注意到,大多数挑战提供的数据分散在多个文件中,而且有些列会在不止一个文件里出现。那么,您首先想到的是什么?当然是把它们“连接”起来!

在本教程中,您将练习几种标准的 pandas 连接技巧。更具体地说,您将学会:

  • 沿行或列拼接(连接)DataFrame。
  • 在特定键上按不同连接逻辑(如 left join、inner join 等)合并 DataFrame。
  • 按索引连接 DataFrame。
  • 使用 pandas 提供的时间序列友好型合并

在此过程中,您还会学到一些在连接前后常用的小技巧。

要点速览

  • 使用 pd.concat() 纵向(行)或横向(列)堆叠 DataFrame
  • 使用 pd.merge() 在共有列上连接 DataFrame,类似 SQL 的 joins
  • 通过 how 参数指定连接类型:'inner'、'outer'、'left' 或 'right'
  • 使用 .join() 基于索引连接 DataFrame
  • 使用 pd.merge_asof() 处理时间序列数据中的“最近键”匹配

pandas 连接

连接和合并 DataFrame 是开展数据分析与机器学习任务的核心步骤之一。每位数据分析师或数据科学家都应熟练掌握这套工具,因为几乎所有情况下,数据都来自多个来源和文件。您可能需要通过某种连接逻辑把所有数据汇总到一起,然后再开始分析。熟悉SQL 连接的人会深知此任务的重要性。即使是要在某些数据上训练机器学习模型,也可能需要先把多个 CSV 文件合并到一个 DataFrame 中。

幸好,Python 中最受欢迎的库之一 pandas 可以助您一臂之力!pandas 提供多种便捷方式,将 Series 和 DataFrame 组合在一起,并支持基于索引的集合逻辑以及面向连接/合并操作的关系代数功能。

在线运行和编辑此教程中的代码

运行代码

pandas 拼接(Concatenate)

先导入本教程中将一直使用的库:pandas

import pandas as pd

本教程中的所有操作都将在您创建的示例 DataFrame 上完成。要创建一个 DataFrame,您可以使用一个Python 字典,例如:

dummy_data1 = {
        'id': ['1', '2', '3', '4', '5'],
        'Feature1': ['A', 'C', 'E', 'G', 'I'],
        'Feature2': ['B', 'D', 'F', 'H', 'J']}

这里,字典 dummy_data1 的键是列名,列表中的值对应每条观测或每一行的数据。要将其转换为 pandas DataFrame,您可以使用 pandas 的 DataFrame() 函数,并通过 columns 参数指定列名:

df1 = pd.DataFrame(dummy_data1, columns = ['id', 'Feature1', 'Feature2'])

df1
 idFeature1Feature2
01AB
12CD
23EF
34GH
45IJ

如您所见,现在有一个包含三列 id、Feature1 和 Feature2 的 DataFrame。还有一列未命名列,是 pandas 自动创建的行标签。与前面的 DataFrame df1 类似,您将再创建两个 DataFrame:df2 和 df3:

dummy_data2 = {
        'id': ['1', '2', '6', '7', '8'],
        'Feature1': ['K', 'M', 'O', 'Q', 'S'],
        'Feature2': ['L', 'N', 'P', 'R', 'T']}
df2 = pd.DataFrame(dummy_data2, columns = ['id', 'Feature1', 'Feature2'])

df2
 idFeature1Feature2
01KL
12MN
26OP
37QR
48ST
dummy_data3 = {
        'id': ['1', '2', '3', '4', '5', '7', '8', '9', '10', '11'],
        'Feature3': [12, 13, 14, 15, 16, 17, 15, 12, 13, 23]}
df3 = pd.DataFrame(dummy_data3, columns = ['id', 'Feature3'])

df3
 idFeature3
0112
1213
2314
3415
4516
5717
6815
7912
81013
91123

 concat()

如果只是想沿行简单地拼接 DataFrame,可以使用 pandas 的 concat() 函数。您需要把要拼接的 DataFrame 名称以列表形式传入 concat() 的参数:

df_row = pd.concat([df1, df2])

df_row
 idFeature1Feature2
01AB
12CD
23EF
34GH
45IJ
01KL
12MN
26OP
37QR
48ST

您会看到,两个 DataFrame(df1 和 df2)已沿行拼接为一个新的 DataFrame(df_row)。不过行标签看起来不太对。如果希望行标签根据拼接自动调整,请在调用 concat() 时将 ignore_index 参数设置为 True:

df_row_reindex = pd.concat([df1, df2], ignore_index=True)

df_row_reindex
 idFeature1Feature2
01AB
12CD
23EF
34GH
45IJ
51KL
62MN
76OP
87QR
98ST

现在行标签正确了!

pandas 还提供了在拼接后为各个 DataFrame 打上标签的选项,这样您就能知道每条数据来自哪个 DataFrame。为此,可以通过 keys 传入标签名列表。在这里,仍对 df1 和 df2 进行相同的拼接,并将其键分别设为 x 和 y。

frames = [df1,df2]
df_keys = pd.concat(frames, keys=['x', 'y'])

df_keys
  idFeature1Feature2
x01AB
12CD
23EF
34GH
45IJ
y01KL
12MN
26OP
37QR
48ST

通过指定 keys,检索某个特定 DataFrame 的数据也更容易。例如可以使用 loc 方法取回标签为 y 的 df2 数据:

df_keys.loc['y']
 idFeature1Feature2
01KL
12MN
26OP
37QR
48ST

您也可以向 concat() 传入字典,此时字典的键将被用作 keys 参数(除非另行指定 keys):

pieces = {'x': df1, 'y': df2}

df_piece = pd.concat(pieces)

df_piece
  idFeature1Feature2
x01AB
12CD
23EF
34GH
45IJ
y01KL
12MN
26OP
37QR
48ST

值得注意的是,concat() 会对数据进行完整拷贝,频繁使用可能造成明显的性能开销。对于大规模数据操作,可考虑了解pandas 2.0 与 polars 在高性能数据处理上的对比。如果需要对多个数据集执行此操作,请使用列表推导式。

frames = [ process_your_file(f) for f in files ]
result = pd.concat(frames)

要沿列拼接 DataFrame,可以将 axis 参数指定为 1:

df_col = pd.concat([df1,df2], axis=1)

df_col
 idFeature1Feature2idFeature1Feature2
01AB1KL
12CD2MN
23EF6OP
34GH7QR
45IJ8ST

pandas 合并 DataFrame

与 DataFrame 相关的另一类常见操作是合并(merge)。两个 DataFrame 可能基于某个共同特征/列,保存关于同一实体的不同信息。为连接这些 DataFrame,pandas 提供了多种函数,如 concat()、merge()、join() 等。本节将练习使用 pandas 的 merge() 函数。

您可以将沿行拼接得到的 df_row(由 df1 和 df2 拼接而成)与 df3 在共有列(或键)id 上进行连接。为此,将 DataFrame 名称和 on 参数(共有列名,这里为 id)传给 merge() 函数:

df_merge_col = pd.merge(df_row, df3, on='id')

df_merge_col
 idFeature1Feature2Feature3
01AB12
11KL12
22CD13
32MN13
43EF14
54GH15
65IJ16
77QR17
88ST15

可以看到,两个 DataFrame 已根据二者 id 列中的共有值合并为一个 DataFrame。比如,这里 id 值 1 在 df_row 中同时对应 A、B 和 K、L,因此在最终的 df_merge_col 中该 id 出现了两次;对应的 Feature3 值 12 则来自 df3。

有时,您用于合并的列在两个 DataFrame 中可能名称不同(本例中相同)。这种情况下,需要通过 left_on 指定左侧 DataFrame 的列名、right_on 指定右侧 DataFrame 的列名,例如:

df_merge_difkey = pd.merge(df_row, df3, left_on='id', right_on='id')

df_merge_difkey
 idFeature1Feature2Feature3
01AB12
11KL12
22CD13
32MN13
43EF14
54GH15
65IJ16
77QR17
88ST15

您也可以使用 pd.concat() 向 DataFrame 添加新行。先用行数据创建一个新的 DataFrame,再与原 DataFrame 拼接即可:

add_row = pd.DataFrame([['10', 'X1', 'X2', 'X3']], 
                       columns=['id', 'Feature1', 'Feature2', 'Feature3'])

df_add_row = pd.concat([df_merge_col, add_row], ignore_index=True)

df_add_row
 idFeature1Feature2Feature3
01AB12
11KL12
22CD13
32MN13
43EF14
54GH15
65IJ16
77QR17
88ST15
910X1X2X3

pandas 连接类型

本节将练习在共有列/键上合并 pandas DataFrame 的多种连接逻辑。这些逻辑与在 SQL 中连接表的方式非常相似。如果您熟悉SQL 中的内连接与外连接,会觉得这些概念很亲切。

连接类型SQL 等价结果使用场景
innerINNER JOIN仅来自两个 DataFrame 的匹配行当您只需要两个表中都存在的记录时
leftLEFT OUTER JOIN左表全部行 + 右表匹配行保留主 DataFrame 的全部记录
rightRIGHT OUTER JOIN右表全部行 + 左表匹配行保留次要 DataFrame 的全部记录
outerFULL OUTER JOIN两个 DataFrame 的所有行当您需要两个表的完全并集时

全外连接(Full Outer Join)

FULL OUTER JOIN 相当于左外连接和右外连接结果的并集。合并后的 DataFrame 将包含两个 DataFrame 的所有记录,并在任一侧缺少匹配时填充NaN 值。在 merge() 中将 how 参数指定为 outer 即可执行全外连接:

df_outer = pd.merge(df1, df2, on='id', how='outer')

df_outer
 idFeature1_xFeature2_xFeature1_yFeature2_y
01ABKL
12CDMN
23EFNaNNaN
34GHNaNNaN
45IJNaNNaN
56NaNNaNOP
67NaNNaNQR
78NaNNaNST

您会发现,结果 DataFrame 包含了两个表的所有记录,且在任一侧没有匹配时以 NaN 填充。另一个需要注意的是,为区分列来源而自动追加的列名后缀。默认后缀是 x 和 y,不过您可以通过 merge() 的 suffixes 参数进行自定义:

df_suffix = pd.merge(df1, df2, left_on='id',right_on='id',how='outer',suffixes=('_left','_right'))

df_suffix
 idFeature1_leftFeature2_leftFeature1_rightFeature2_right
01ABKL
12CDMN
23EFNaNNaN
34GHNaNNaN
45IJNaNNaN
56NaNNaNOP
67NaNNaNQR
78NaNNaNST

内连接(Inner Join)

INNER JOIN 仅保留在 DataFrame A 和 DataFrame B 中都匹配的记录。将 merge() 的 how 参数设为 inner 即可执行内连接:

df_inner = pd.merge(df1, df2, on='id', how='inner')

df_inner
 idFeature1_xFeature2_xFeature1_yFeature2_y
01ABKL
12CDMN

右连接(Right Join)

RIGHT JOIN 会产生来自 DataFrame B(右侧 DataFrame)的完整记录集,并在可匹配的地方补上 DataFrame A(左侧 DataFrame)的记录。若无匹配,右侧将包含空值。将 merge() 的 how 参数设为 right 即可执行右连接:

df_right = pd.merge(df1, df2, on='id', how='right')

df_right
 idFeature1_xFeature2_xFeature1_yFeature2_y
01ABKL
12CDMN
26NaNNaNOP
37NaNNaNQR
48NaNNaNST

左连接(Left Join)

LEFT JOIN 会产生来自 DataFrame A(左侧 DataFrame)的完整记录集,并在可匹配的地方补上 DataFrame B(右侧 DataFrame)的记录。若无匹配,左侧将包含空值。将 merge() 的 how 参数设为 left 即可执行左连接:

df_left = pd.merge(df1, df2, on='id', how='left')

df_left
 idFeature1_xFeature2_xFeature1_yFeature2_y
01ABKL
12CDMN
23EFNaNNaN
34GHNaNNaN
45IJNaNNaN

按索引连接

有时您需要基于索引或行标签进行连接。此时需要将 right_index(右侧 DataFrame 的索引)和 left_index(左侧 DataFrame 的索引)设为 True:

df_index = pd.merge(df1, df2, right_index=True, left_index=True)

df_index
 id_xFeature1_xFeature2_xid_yFeature1_yFeature2_y
01AB1KL
12CD2MN
23EF6OP
34GH7QR
45IJ8ST

pandas Join

pandas 的 DataFrame.join() 方法用于在唯一索引上连接 DataFrame。您可以使用可选参数 on 指定基于索引进行连接的列名(们),并通过 how 参数控制两个对象的连接方式。默认使用内连接。

pandas 连接两个 DataFrame

我们来用 .join 连接两个数据框。这里提供了 `lsuffix` 和 `rsuffix` 以避免列名重叠报错。它是基于索引而非列进行连接,因此需要更改 ‘id’ 列或提供后缀。

df2.join(df3, lsuffix='_left', rsuffix='_right')

 id_leftFeature1Feature2id_rightFeature3
01KL112
12MN213
26OP314
37QR415
48ST516

我们也可以使用 on 参数在索引上连接列。要成功应用该连接,需要将 df3 的 ‘id’ 列设为索引,并在 on 参数中提供 ‘id’ 列。默认情况下,它会使用左连接。

df2.join(df3.set_index('id'), on='id')

 idFeature1Feature2Feature3
01KL12.0
12MN13.0
26OPNaN
37QR17.0
48ST15.0

与 merge 函数类似,我们可以通过 how 参数改变 join 的方式。这里将使用内连接。

df2.join(df3.set_index('id'), on='id', how = "inner")

 id_leftFeature1Feature2Feature3
01KL12
12MN13
37QR17
48ST15

时间序列友好型合并

pandas 为合并时间序列 DataFrame 提供了特殊函数,其中最常用也最有用的是 merge_asof()。merge_asof() 类似于有序的左连接,但匹配的是最近的键而非完全相等的键。对于左侧 DataFrame 的每一行,它会选择右侧 DataFrame 中 on 键小于左侧键的最后一行。两个 DataFrame 都必须按该键排序。

可选地,asof 合并可以执行分组级别的合并。它在对 on 键进行最近匹配的同时,还要求 by 键相等。

例如,您可能有交易数据(trades)和报价数据(quotes),希望对它们进行 asof 合并。这里将左侧 DataFrame 设为 trades,右侧 DataFrame 设为 quotes。它们按时间键 time 进行 asof 合并,并按股票代码进行分组合并。

trades = pd.DataFrame({
    'time': pd.to_datetime(['20160525 13:30:00.023',
                            '20160525 13:30:00.038',
                            '20160525 13:30:00.048',
                            '20160525 13:30:00.048',
                            '20160525 13:30:00.048']),
    'ticker': ['MSFT', 'MSFT','GOOG', 'GOOG', 'AAPL'],
    'price': [51.95, 51.95,720.77, 720.92, 98.00],
    'quantity': [75, 155,100, 100, 100]},
    columns=['time', 'ticker', 'price', 'quantity'])

quotes = pd.DataFrame({
    'time': pd.to_datetime(['20160525 13:30:00.023',
                            '20160525 13:30:00.023',
                            '20160525 13:30:00.030',
                            '20160525 13:30:00.041',
                            '20160525 13:30:00.048',
                            '20160525 13:30:00.049',
                            '20160525 13:30:00.072',
                            '20160525 13:30:00.075']),
    'ticker': ['GOOG', 'MSFT', 'MSFT','MSFT', 'GOOG', 'AAPL', 'GOOG','MSFT'],
    'bid': [720.50, 51.95, 51.97, 51.99,720.50, 97.99, 720.50, 52.01],
    'ask': [720.93, 51.96, 51.98, 52.00,720.93, 98.01, 720.88, 52.03]},
    columns=['time', 'ticker', 'bid', 'ask'])
trades
 timetickerpricequantity
02016-05-25 13:30:00.023MSFT51.9575
12016-05-25 13:30:00.038MSFT51.95155
22016-05-25 13:30:00.048GOOG720.77100
32016-05-25 13:30:00.048GOOG720.92100
42016-05-25 13:30:00.048AAPL98.00100
quotes
 timetickerbidask
02016-05-25 13:30:00.023GOOG720.50720.93
12016-05-25 13:30:00.023MSFT51.9551.96
22016-05-25 13:30:00.030MSFT51.9751.98
32016-05-25 13:30:00.041MSFT51.9952.00
42016-05-25 13:30:00.048GOOG720.50720.93
52016-05-25 13:30:00.049AAPL97.9998.01
62016-05-25 13:30:00.072GOOG720.50720.88
72016-05-25 13:30:00.075MSFT52.0152.03
df_merge_asof = pd.merge_asof(trades, quotes,
              on='time',
              by='ticker')

df_merge_asof
 timetickerpricequantitybidask
02016-05-25 13:30:00.023MSFT51.957551.9551.96
12016-05-25 13:30:00.038MSFT51.9515551.9751.98
22016-05-25 13:30:00.048GOOG720.77100720.50720.93
32016-05-25 13:30:00.048GOOG720.92100720.50720.93
42016-05-25 13:30:00.048AAPL98.00100NaNNaN

如果您细心观察,会发现 AAPL 这一行出现 NaN 的原因。因为右侧的 quotes DataFrame 中,对于 AAPL,并不存在一个时间小于左表 13:30:00.048 的记录,所以 bid 和 ask 列被填充为 NaN。

您还可以为时间列设置预定义的容差水平。假设您只希望报价时间与成交时间在 2ms 以内才进行 asof 合并,那么需要指定 tolerance 参数:

df_merge_asof_tolerance = pd.merge_asof(trades, quotes,
              on='time',
              by='ticker',
              tolerance=pd.Timedelta('2ms'))

df_merge_asof_tolerance
 timetickerpricequantitybidask
02016-05-25 13:30:00.023MSFT51.957551.9551.96
12016-05-25 13:30:00.038MSFT51.95155NaNNaN
22016-05-25 13:30:00.048GOOG720.77100720.50720.93
32016-05-25 13:30:00.048GOOG720.92100720.50720.93
42016-05-25 13:30:00.048AAPL98.00100NaNNaN

注意与上一个结果的差异:若时间差超过 2ms 的容差阈值,行将不会被合并。

结论

在本教程中,您学习了如何使用 pandas 库的 concat() 和 merge() 函数,基于多种逻辑对 DataFrame 进行拼接与合并。最后还实践了用于时间序列 DataFrame 的特殊函数 merge_asof()。在此过程中,您也学习了如何处理DataFrame 索引。pandas 中还有许多用于连接 DataFrame 的选项,建议您查阅其精彩的官方文档。祝您探索愉快!

本教程参考了这份 pandas 文档。

如果您想进一步学习 pandas,请参加 DataCamp 的 Data Manipulation with pandas 课程,并查看我们的 Python Pandas DataFrame 教程。

DataCamp 还提供多篇实用的 pandas 教程,包括:

学习愉快!

常见问题

pandas 中 merge() 与 join() 有何区别?

merge() 更灵活,可在任意列上进行连接,类似 SQL 的连接。join() 主要用于基于索引的连接,是对 merge() 的便捷封装。若按列连接,使用 merge();若连接键已设为索引,使用 join()。

如何在 pandas 中基于多列进行合并?

将列名列表传给 on 参数:pd.merge(df1, df2, on=['col1', 'col2'])。如果两个 DataFrame 的列名不同,使用带列表的 left_on 和 right_on:pd.merge(df1, df2, left_on=['a', 'b'], right_on=['c', 'd'])。

合并 DataFrame 时缺失值会怎样处理?

具体行为取决于连接类型。inner 连接会排除无匹配的行。left、right 或 outer 连接会保留未匹配行,并在来自另一 DataFrame 的列中填充 NaN。您可以在合并后使用 fillna() 或 dropna() 进行处理。

在 pandas 中,何时应使用 concat(),何时应使用 merge()?

使用 concat() 根据索引对齐,纵向(添加行)或横向(添加列)拼接 DataFrame。需要基于共有列值进行组合、类似 SQL 连接时使用 merge()。可将 concat() 理解为把 DataFrame “粘”在一起,而 merge() 则是关系型连接。

合并后遇到重复列名应如何处理?

默认情况下,pandas 会对重复的列名添加后缀 _x 和 _y。可通过 suffixes 参数自定义:pd.merge(df1, df2, on='id', suffixes=('_left', '_right'))。合并后,您可以使用 df.rename(columns={'old': 'new'}) 重命名列,或用 df.drop(columns=['col_y']) 删除重复列。

pandas 中 merge() 与 join() 有何区别?

merge() 更灵活,可在任意列上进行连接,类似 SQL 的连接。join() 主要用于基于索引的连接,是对 merge() 的便捷封装。若按列连接,使用 merge();若连接键已设为索引,使用 join()。

如何在 pandas 中基于多列进行合并?

将列名列表传给 on 参数:pd.merge(df1, df2, on=['col1', 'col2'])。如果两个 DataFrame 的列名不同,使用带列表的 left_on 和 right_on:pd.merge(df1, df2, left_on=['a', 'b'], right_on=['c', 'd'])。

合并 DataFrame 时缺失值会怎样处理?

具体行为取决于连接类型。inner 连接会排除无匹配的行。left、right 或 outer 连接会保留未匹配行,并在来自另一 DataFrame 的列中填充 NaN。您可以在合并后使用 fillna() 或 dropna() 进行处理。

在 pandas 中,何时应使用 concat(),何时应使用 merge()?

使用 concat() 根据索引对齐,纵向(添加行)或横向(添加列)拼接 DataFrame。需要基于共有列值进行组合、类似 SQL 连接时使用 merge()。可将 concat() 理解为把 DataFrame “粘”在一起,而 merge() 则是关系型连接。

合并后遇到重复列名应如何处理?

默认情况下,pandas 会对重复的列名添加后缀 _x 和 _y。可通过 suffixes 参数自定义:pd.merge(df1, df2, on='id', suffixes=('_left', '_right'))。合并后,您可以使用 df.rename(columns={'old': 'new'}) 重命名列,或用 df.drop(columns=['col_y']) 删除重复列。

主题