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)的列表或字典 | 必需 |
axis | 0 堆叠行(纵向),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_b(df_b 中没有匹配的索引),第 3 行没有 value_a(df_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 添加行 |
| 输入数量 | 一次处理任意数量 | 一次处理两个 | 一次处理两个 |
| 匹配逻辑 | 按索引(或列名)对齐 | 按键列值匹配 | 按列名对齐 |
| 连接类型 | outer、inner | inner、left、right、outer、cross | 仅 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 ValueError4. 意外沿错误轴连接
如果你的结果是列数翻倍而不是行数翻倍(或反之),检查 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=True:pd.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) 等工具可以让你无需编写图表代码就能直观地探索合并结果,更快地验证数据管道并发现跨数据源的模式。