Skip to content
话题
Pandas
Pandas Concat:如何在 Python 中连接 DataFrame

Pandas Concat:如何在 Python 中连接 DataFrame

Updated on

现实世界的数据很少存放在单个文件中。你从一个 CSV 中提取一月份的销售数据,从另一个 CSV 中提取二月份的数据,从第三个文件中提取第一季度的目标。你抓取多个网页到不同的 DataFrame 中。你为了并行处理而分割大型数据集,之后需要重新组装这些片段。在每种情况下,你都需要一种可靠的方法来合并 DataFrame,同时不丢失行、不打乱列、不损坏索引。

pandas concat 函数(pd.concat())是完成这项工作的标准工具。它可以纵向(添加行)或横向(添加列)堆叠 DataFrame,优雅地处理不匹配的列,并且能在单次调用中处理任意数量的 DataFrame。本指南涵盖了你需要的每个参数,配有可直接粘贴到 notebook 中的可运行代码示例。

📚

pd.concat() 的功能 -- 基本语法

pd.concat() 接受一个 DataFrame 的列表(或字典),并沿指定轴将它们拼接在一起。你可以把它想象成堆积木 -- 纵向堆叠会添加更多行,横向并排会添加更多列。

import pandas as pd
 
result = pd.concat(
    objs,              # list or dict of DataFrames
    axis=0,            # 0 = vertical (rows), 1 = horizontal (columns)
    join='outer',      # 'outer' or 'inner'
    ignore_index=False,
    keys=None,
    sort=False,
    verify_integrity=False
)

关键参数一览

参数描述默认值
objs要连接的 DataFrame(或 Series)的列表或字典必需
axis0 堆叠行(纵向),1 堆叠列(横向)0
join'outer' 保留所有列;'inner' 仅保留共有列'outer'
ignore_index如果为 True,将结果的索引重置为 0, 1, 2, ...False
keys用于标识每行来自哪个原始 DataFrame 的标签None
sort对非连接轴排序(axis=0 时为列名)False
verify_integrity如果结果存在重复索引值则抛出错误False

所有示例的样本数据

以下所有示例都使用这些 DataFrame:

import pandas as pd
 
df_jan = pd.DataFrame({
    'product': ['Widget', 'Gadget', 'Sprocket'],
    'units_sold': [150, 200, 80],
    'revenue': [1500, 3000, 960]
})
 
df_feb = pd.DataFrame({
    'product': ['Widget', 'Gadget', 'Sprocket'],
    'units_sold': [170, 180, 95],
    'revenue': [1700, 2700, 1140]
})
 
print(df_jan)
print(df_feb)

输出:

    product  units_sold  revenue
0    Widget         150     1500
1    Gadget         200     3000
2  Sprocket          80      960

    product  units_sold  revenue
0    Widget         170     1700
1    Gadget         180     2700
2  Sprocket          95     1140

纵向连接 DataFrame(axis=0)

纵向连接是最常见的用例。它将一个 DataFrame 堆叠到另一个之上,添加行。当你有月度文件、批处理结果或任何分布在具有相同列的多个表中的数据时,就会用到它。

combined = pd.concat([df_jan, df_feb])
print(combined)

输出:

    product  units_sold  revenue
0    Widget         150     1500
1    Gadget         200     3000
2  Sprocket          80      960
0    Widget         170     1700
1    Gadget         180     2700
2  Sprocket          95     1140

注意索引:两个 DataFrame 都保留了它们原始的索引值(0, 1, 2),因此结果中存在重复的索引值。这通常不是你想要的结果。解决方法是 ignore_index 参数,将在下一节介绍。

ignore_index 参数 -- 重置索引

设置 ignore_index=True 会丢弃原始索引,并分配一个从 0 开始的新顺序索引:

combined = pd.concat([df_jan, df_feb], ignore_index=True)
print(combined)

输出:

    product  units_sold  revenue
0    Widget         150     1500
1    Gadget         200     3000
2  Sprocket          80      960
3    Widget         170     1700
4    Gadget         180     2700
5  Sprocket          95     1140

何时使用: 纵向连接时几乎总是需要使用。除非你的索引包含有意义的信息(如时间戳或唯一 ID),否则应重置索引以避免后续混淆。

keys 参数 -- 创建层级索引

keys 参数为索引添加一个层级,用于标识每行来自哪个数据源。这会创建一个 MultiIndex(层级索引):

combined = pd.concat([df_jan, df_feb], keys=['January', 'February'])
print(combined)

输出:

               product  units_sold  revenue
January  0      Widget         150     1500
         1      Gadget         200     3000
         2    Sprocket          80      960
February 0      Widget         170     1700
         1      Gadget         180     2700
         2    Sprocket          95     1140

然后你可以使用 .loc 从特定源选择数据:

# Get only January data
jan_data = combined.loc['January']
print(jan_data)

输出:

    product  units_sold  revenue
0    Widget         150     1500
1    Gadget         200     3000
2  Sprocket          80      960

何时使用: 当你需要追踪每行属于哪个原始 DataFrame 时使用 keys -- 例如,来自不同实验、时间段或数据源的数据。

横向连接(axis=1) -- 并排放置

设置 axis=1 将 DataFrame 并排放置,添加列。Pandas 按索引值对齐行。

targets = pd.DataFrame({
    'target_units': [160, 190, 90],
    'target_revenue': [1600, 2850, 1080]
})
 
result = pd.concat([df_jan, targets], axis=1)
print(result)

输出:

    product  units_sold  revenue  target_units  target_revenue
0    Widget         150     1500           160            1600
1    Gadget         200     3000           190            2850
2  Sprocket          80      960            90            1080

这之所以能正常工作,是因为两个 DataFrame 共享相同的索引(0, 1, 2)。如果索引不对齐,不匹配的行将出现 NaN 值:

df_a = pd.DataFrame({'value_a': [10, 20, 30]}, index=[0, 1, 2])
df_b = pd.DataFrame({'value_b': [40, 50, 60]}, index=[1, 2, 3])
 
result = pd.concat([df_a, df_b], axis=1)
print(result)

输出:

   value_a  value_b
0     10.0      NaN
1     20.0     40.0
2     30.0     50.0
3      NaN     60.0

第 0 行没有 value_bdf_b 中没有匹配的索引),第 3 行没有 value_adf_a 中没有匹配的索引)。

join 参数 -- Inner vs Outer

join 参数控制当 DataFrame 具有不同列(axis=0 时)或不同索引值(axis=1 时)时的行为。

outer join(默认) -- 保留所有内容

df_with_extra = pd.DataFrame({
    'product': ['Widget', 'Gadget'],
    'units_sold': [200, 250],
    'region': ['East', 'West']
})
 
result = pd.concat([df_jan, df_with_extra], join='outer', ignore_index=True)
print(result)

输出:

    product  units_sold  revenue region
0    Widget         150   1500.0    NaN
1    Gadget         200   3000.0    NaN
2  Sprocket          80    960.0    NaN
3    Widget         200      NaN   East
4    Gadget         250      NaN   West

两个 DataFrame 的所有列都会出现。缺失值用 NaN 填充。

inner join -- 仅保留共有列

result = pd.concat([df_jan, df_with_extra], join='inner', ignore_index=True)
print(result)

输出:

   product  units_sold
0   Widget         150
1   Gadget         200
2 Sprocket          80
3   Widget         200
4   Gadget         250

只有在两个 DataFrame 中都存在的列会保留。revenue 列(df_with_extra 中不存在)和 region 列(df_jan 中不存在)都被删除。

何时使用 inner join: 当你想要没有 NaN 值的干净结果,并且愿意丢失不在每个 DataFrame 中都出现的列时。

连接 DataFrame 列表

pd.concat() 相对于其他合并方法的最大优势之一是,它能在单次调用中处理任意数量的 DataFrame。这是在循环中加载文件后进行合并的标准模式:

import pandas as pd
 
# Simulate loading monthly CSV files
months = {
    'Jan': {'product': ['Widget', 'Gadget'], 'units': [150, 200]},
    'Feb': {'product': ['Widget', 'Gadget'], 'units': [170, 180]},
    'Mar': {'product': ['Widget', 'Gadget'], 'units': [190, 210]},
}
 
dfs = []
for month, data in months.items():
    df = pd.DataFrame(data)
    df['month'] = month
    dfs.append(df)
 
all_data = pd.concat(dfs, ignore_index=True)
print(all_data)

输出:

  product  units month
0  Widget    150   Jan
1  Gadget    200   Jan
2  Widget    170   Feb
3  Gadget    180   Feb
4  Widget    190   Mar
5  Gadget    210   Mar

这种模式 -- 构建 DataFrame 列表,然后在最后调用一次 concat -- 比在循环中逐个追加 DataFrame 要快得多。每次 append 都会创建完整的副本,而单次 pd.concat() 调用只需分配一次内存。

concat vs merge vs append -- 对比表

Pandas 提供了多种合并 DataFrame 的方法。选择正确的方法取决于你如何想要合并它们:

特性pd.concat()pd.merge()DataFrame.append()
主要用途堆叠 DataFrame(行或列)按共享列值连接(类似 SQL)向 DataFrame 添加行
输入数量一次处理任意数量一次处理两个一次处理两个
匹配逻辑按索引(或列名)对齐按键列值匹配按列名对齐
连接类型outerinnerinnerleftrightoutercross仅 outer
默认行为outer join,纵向堆叠inner join,按共享列outer join,追加行
最适合合并月度文件、批处理结果、相同模式的表关系型连接(客户 + 订单)自 pandas 2.0 起已弃用
性能处理多个 DataFrame 时快速针对两表连接优化慢(每次调用都复制数据)

何时使用哪种方法

  • 使用 pd.concat() -- 当你的 DataFrame 具有相同结构(相同列)并且你想堆叠它们时。也可用于按索引对齐的横向连接。
  • 使用 pd.merge() -- 当你需要基于列值匹配行时 -- 例如通过 product_id 连接销售表和产品表。详情请参阅我们的 pandas merge 指南
  • 避免使用 DataFrame.append() -- 它在 pandas 1.4 中被弃用,在 pandas 2.0 中被移除。请改用 pd.concat([df1, df2])

常见错误及修复方法

1. 列不对齐

连接具有不同列名的 DataFrame 时,默认的 outer join 会用 NaN 填充缺失值。如果这不是你预期的结果,检查列名:

# Diagnose: compare column names
print(df1.columns.tolist())
print(df2.columns.tolist())
 
# Fix: rename columns to match before concatenating
df2 = df2.rename(columns={'sales': 'revenue', 'qty': 'units_sold'})
combined = pd.concat([df1, df2], ignore_index=True)

2. 连接后数据类型不匹配

如果一个 DataFrame 将列存储为 int64,另一个存储为 float64,pandas 会向上转换为 float。更糟糕的是,如果其中一个存储为字符串,你会得到 object 类型的列:

# Check dtypes after concat
combined = pd.concat([df1, df2], ignore_index=True)
print(combined.dtypes)
 
# Fix: cast before concatenating
df2['units_sold'] = df2['units_sold'].astype(int)
combined = pd.concat([df1, df2], ignore_index=True)

3. 重复的索引值

不使用 ignore_index=True 时,纵向连接会保留原始索引,导致重复值。这会在 .loc 查找时引起问题:

combined = pd.concat([df1, df2])
# combined.loc[0] returns TWO rows, not one
 
# Fix option 1: use ignore_index
combined = pd.concat([df1, df2], ignore_index=True)
 
# Fix option 2: use verify_integrity to catch the issue early
combined = pd.concat([df1, df2], verify_integrity=True)  # raises ValueError

4. 意外沿错误轴连接

如果你的结果是列数翻倍而不是行数翻倍(或反之),检查 axis 参数:

# Wrong: this adds columns side by side
wrong = pd.concat([df1, df2], axis=1)
 
# Right: this stacks rows vertically
right = pd.concat([df1, df2], axis=0)

使用 PyGWalker 可视化连接后的 DataFrame

从多个来源连接数据后,你通常需要验证结果并探索合并数据集中的模式。与其用 matplotlib 或 seaborn 编写手动绘图代码,不如使用 PyGWalker (opens in a new tab) -- 一个开源 Python 库,可以将任何 pandas DataFrame 直接在 Jupyter Notebook 中转换为交互式的、类似 Tableau 的可视化探索界面。

import pandas as pd
import pygwalker as pyg
 
# Combine monthly sales data
df_jan = pd.DataFrame({
    'product': ['Widget', 'Gadget', 'Sprocket'],
    'units_sold': [150, 200, 80],
    'revenue': [1500, 3000, 960],
    'month': ['Jan', 'Jan', 'Jan']
})
 
df_feb = pd.DataFrame({
    'product': ['Widget', 'Gadget', 'Sprocket'],
    'units_sold': [170, 180, 95],
    'revenue': [1700, 2700, 1140],
    'month': ['Feb', 'Feb', 'Feb']
})
 
combined = pd.concat([df_jan, df_feb], ignore_index=True)
 
# Launch interactive visualization
walker = pyg.walk(combined)

使用 PyGWalker,你可以将 product 拖到 x 轴,将 revenue 拖到 y 轴,然后按 month 分割,即可立即比较不同时期的收入趋势 -- 无需编写图表代码。你只需拖拽字段就可以创建柱状图、散点图、折线图等。它对于验证连接是否正确以及不同来源的数据是否按预期对齐特别有用。

使用 pip install pygwalker 安装 PyGWalker,或在 Google Colab (opens in a new tab)Kaggle (opens in a new tab) 上试用。

常见问题

pandas concat 和 merge 有什么区别?

pd.concat() 通过索引对齐,纵向(添加行)或横向(添加列)堆叠 DataFrame。pd.merge() 通过匹配特定列中的值来连接两个 DataFrame,类似于 SQL JOIN。当你的 DataFrame 具有相同的列并且想要合并行时,使用 concat。当你需要基于共享键列匹配行时,使用 merge。

pd.concat() 会修改原始 DataFrame 吗?

不会。pd.concat() 始终返回一个新的 DataFrame。原始 DataFrame 保持不变。这与 pandas 的设计原则一致,即操作返回新对象而不是就地修改数据。

如何连接具有不同列的 DataFrame?

使用默认 join='outer'pd.concat() -- 它保留所有 DataFrame 的所有列,并用 NaN 填充缺失值。如果你只想要每个 DataFrame 中都存在的列,设置 join='inner'。你也可以在连接前重命名列以确保对齐。

pd.concat() 比 DataFrame.append() 快吗?

是的。DataFrame.append() 在 pandas 1.4 中被弃用,在 pandas 2.0 中被移除。它内部调用 pd.concat(),但每次调用都创建一个副本。合并多个 DataFrame 时,将它们收集到列表中并调用一次 pd.concat() 要快得多,因为它只分配一次内存。

连接后如何重置索引?

pd.concat() 传递 ignore_index=Truepd.concat([df1, df2], ignore_index=True)。这会将原始索引值替换为从 0 开始的新顺序索引。或者,对结果调用 .reset_index(drop=True)

总结

pandas concat() 函数是合并具有相同结构的 DataFrame 的首选工具。以下是关键要点:

  • 纵向连接axis=0)堆叠行,是最常见的用例 -- 非常适合合并月度文件、批处理结果或分割的数据集。
  • 横向连接axis=1)将 DataFrame 并排放置,按索引对齐。
  • 使用 ignore_index=True 获得干净的顺序索引(大多数情况下推荐)。
  • 使用 keys 创建层级索引,追踪每行来自哪个数据源。
  • join 参数控制如何处理不匹配的列:'outer' 保留所有内容,'inner' 仅保留共有列。
  • 始终将 DataFrame 收集到列表中,调用一次 pd.concat(),而不是在循环中逐个追加。
  • 当你需要基于列值进行 SQL 风格的连接时,使用 pd.merge()

数据连接完成后,PyGWalker (opens in a new tab) 等工具可以让你无需编写图表代码就能直观地探索合并结果,更快地验证数据管道并发现跨数据源的模式。

📚