真实分析中,数据往往分散在多张表里:订单表、客户表、商品表……把它们合起来是必修课。本篇文章讲解 pandas 的三大合并武器:concat(拼接)、merge(SQL 式连接)、join(索引连接)。
目录
1. 三种合并方式总览
2. concat:纵向/横向拼接
3. merge:SQL 式表连接
4. join:基于索引的连接
5. 合并后去重与索引处理
6. 实战:订单表 × 客户表 × 商品表
7. 常见坑与注意事项
8. 本章小结与练习
1. 三种合并方式总览
| 方法 | 连接依据 | 典型场景 |
|------|----------|----------|
| concat | 直接堆叠(行或列) | 多个月的数据纵向拼接、多张表横向拼列 |
| merge | 指定列(类似 SQL JOIN) | 订单表按客户ID 关联客户表 |
| join | 索引 | 两表行索引一致时的关联 |
记忆:concat 是"叠",merge 是"按列对齐接",join 是"按索引接"。
准备示例数据:
import pandas as pd
# 一月、二月销售
jan = pd.DataFrame({"月份": ["1月", "1月"], "商品": ["苹果", "香蕉"], "销量": [100, 80]})
feb = pd.DataFrame({"月份": ["2月", "2月"], "商品": ["苹果", "橙子"], "销量": [120, 90]})
# 商品信息表
products = pd.DataFrame({
"商品": ["苹果", "香蕉", "橙子"],
"单价": [5, 3, 4],
"产地": ["山东", "海南", "江西"],
})
# 带索引的表
price_2025 = pd.DataFrame({"单价": [5, 3, 4]}, index=["苹果", "香蕉", "橙子"])
2. concat:纵向/横向拼接
2.1 纵向拼接(默认 axis=0,行增加)
all_sales = pd.concat([jan, feb], ignore_index=True)
print(all_sales)
月份 商品 销量
0 1月 苹果 100
1 1月 香蕉 80
2 2月 苹果 120
3 2月 橙子 90
ignore_index=True 让索引连续从 0 开始,否则会保留各自的索引(0,1,0,1)。2.2 横向拼接(axis=1,列增加)
df_a = pd.DataFrame({"a": [1, 2]})
df_b = pd.DataFrame({"b": [3, 4]})
print(pd.concat([df_a, df_b], axis=1))
# a b
# 0 1 3
# 1 2 4
2.3 列不一致时的处理(join 参数)
print(pd.concat([jan, feb], join="outer", ignore_index=True)) # 默认 outer:缺的列补 NaN
print(pd.concat([jan, feb], join="inner", ignore_index=True)) # inner:只保留共有列
2.4 标记来源(keys 参数)
all_sales = pd.concat([jan, feb], keys=["1月", "2月"])
print(all_sales)
# 月份 商品 销量
# 1月 0 1月 苹果 100
# 1 1月 香蕉 80
# 2月 0 2月 苹果 120
# 1 2月 橙子 90
keys会生成 MultiIndex,之后可用all_sales.loc["1月"]取回某部分。
2.5 多个 DataFrame 批量拼接
frames = [jan, feb] # 可以是从文件循环读出来的列表
big = pd.concat(frames, ignore_index=True)
| 参数 | 作用 |
|------|------|
| axis=0/1 | 纵向(默认)/ 横向 |
| join="outer"/"inner" | 并集 / 交集 |
| ignore_index=True | 重置索引 |
| keys=[...] | 为每个来源加标签 |
| sort=True | 列排序(列不一致时) |
3. merge:SQL 式表连接
merge 是最强大的合并方式,支持一对一、一对多、多对多,以及各种连接类型。
3.1 内连接(how="inner")
# 按"商品"列匹配,只保留两边都有的商品
print(pd.merge(all_sales, products, on="商品", how="inner"))
月份 商品 销量 单价 产地
0 1月 苹果 100 5 山东
1 2月 苹果 120 5 山东
2 1月 香蕉 80 3 海南
3 2月 橙子 90 4 江西
内连接结果:苹果、香蕉、橙子都在商品表中,所以都保留。
3.2 外连接(how="left"/"right"/"outer")
# 左连接:以左表为准,右表没匹配到的补 NaN
print(pd.merge(all_sales, products, on="商品", how="left"))
# 右连接
print(pd.merge(all_sales, products, on="商品", how="right"))
# 全外连接:两边都保留
print(pd.merge(all_sales, products, on="商品", how="outer"))
| how | 含义 | 类比 SQL |
|-----|------|----------|
| inner | 只保留两表都匹配的 | INNER JOIN |
| left | 保留左表全部 | LEFT JOIN |
| right | 保留右表全部 | RIGHT JOIN |
| outer | 两表全部保留 | FULL OUTER JOIN |
3.3 列名不同时指定左右键
orders = pd.DataFrame({"商品名": ["苹果", "橙子"], "数量": [5, 3]})
products2 = products.rename(columns={"商品": "商品名"})
print(pd.merge(orders, products2, left_on="商品名", right_on="商品名"))
如果两边键名都不同:
print(pd.merge(orders, products, left_on="商品名", right_on="商品"))
# 商品名会在结果中同时出现两列(商品名、商品)
不想让两列都保留,可以 suffixes 或合并后删除多余列。3.4 合并后重名列处理(suffixes)
df1 = pd.DataFrame({"id": [1, 2], "分数": [90, 80]})
df2 = pd.DataFrame({"id": [1, 2], "分数": [95, 85]})
print(pd.merge(df1, df2, on="id", suffixes=("_平时", "_期末")))
# id 分数_平时 分数_期末
# 0 1 90 95
# 1 2 80 85
3.5 按多个键合并
# 复合键:同一商品 + 同一月份才算匹配
sales = pd.DataFrame({
"月份": ["1月", "1月", "2月"],
"商品": ["苹果", "香蕉", "苹果"],
"销量": [100, 80, 120],
})
price = pd.DataFrame({
"月份": ["1月", "1月", "2月"],
"商品": ["苹果", "香蕉", "苹果"],
"促销价": [4.5, 2.8, 4.0],
})
print(pd.merge(sales, price, on=["月份", "商品"]))
3.6 索引作为连接键
# 左右键都指向索引
df_left = pd.DataFrame({"a": [1, 2]}, index=["x", "y"])
df_right = pd.DataFrame({"b": [3, 4]}, index=["x", "y"])
print(pd.merge(df_left, df_right, left_index=True, right_index=True))
4. join:基于索引的连接
join 默认按索引连接,是"索引对齐版 merge"的简便写法。
# 按索引连接
sales_by_pd = pd.DataFrame({"销量": [500, 300, 200]}, index=["苹果", "香蕉", "橙子"])
print(price_2025.join(sales_by_pd))
# 单价 销量
# 苹果 5 500
# 香蕉 3 300
# 橙子 4 200
# 左连接(默认 how="left")
print(price_2025.join(sales_by_pd, how="inner"))
# 与普通 DataFrame 按索引 join
df_index = pd.DataFrame({"品类": ["水果", "水果", "水果"]}, index=["苹果", "香蕉", "橙子"])
print(price_2025.join(df_index))
join 与 merge 的关系
# 这两行等价:
df1.join(df2, how="inner")
pd.merge(df1, df2, left_index=True, right_index=True, how="inner")
选择建议:有明确关联列用merge;两边都是索引对齐的用join。
5. 合并后去重与索引处理
big = pd.concat([jan, feb], ignore_index=True)
# 合并后常见收尾
big = big.drop_duplicates() # 去重
big = big.reset_index(drop=True) # 重置索引
big = big.sort_values(["月份", "销量"], ascending=[True, False]) # 排序
# 检查合并是否产生重复(一对多导致的)
print(big.duplicated().sum())
# 检查合并后行数是否异常
print(len(big))
6. 实战:订单表 × 客户表 × 商品表
import pandas as pd
# 三张源表
orders = pd.DataFrame({
"订单号": ["O001", "O002", "O003", "O004"],
"客户ID": ["C1", "C2", "C1", "C3"],
"商品": ["苹果", "香蕉", "橙子", "苹果"],
"数量": [10, 5, 8, 12],
})
customers = pd.DataFrame({
"客户ID": ["C1", "C2", "C3"],
"客户名": ["张三", "李四", "王五"],
"城市": ["北京", "上海", "广州"],
})
products = pd.DataFrame({
"商品": ["苹果", "香蕉", "橙子"],
"单价": [5, 3, 4],
})
# 1. 订单 + 客户(按客户ID 左连接)
step1 = pd.merge(orders, customers, on="客户ID", how="left")
# 2. 再 + 商品(按商品 左连接)
step2 = pd.merge(step1, products, on="商品", how="left")
# 3. 计算金额
step2["金额"] = step2["数量"] * step2["单价"]
print(step2)
订单号 客户ID 商品 数量 客户名 城市 单价 金额
0 O001 C1 苹果 10 张三 北京 5 50
1 O002 C2 香蕉 5 李四 上海 3 15
2 O003 C1 橙子 8 张三 北京 4 32
3 O004 C3 苹果 12 王五 广州 5 60
# 4. 按城市汇总订单金额
print(step2.groupby("城市")["金额"].sum())
# 上海 15
# 北京 82
# 广州 60
链式写法(推荐):
result = (
orders
.merge(customers, on="客户ID", how="left")
.merge(products, on="商品", how="left")
.assign(金额=lambda d: d["数量"] * d["单价"])
)
7. 常见坑与注意事项
| 坑 | 现象 | 解决办法 |
|----|------|----------|
| concat 后索引重复 | 0,1,0,1 混乱 | ignore_index=True |
| merge 键名不匹配 | KeyError | 用 left_on/right_on |
| 一对多合并膨胀 | 行数暴增 | 合并前先 drop_duplicates 检查键 |
| 列名冲突 | 合并后多出 _x/_y | 用 suffixes 或提前重命名 |
| join 忘了 how | 默认 left 丢数据 | 明确 how="inner" 等 |
| merge 非键列相同名 | 结果列重复 | 合并前 rename 或 suffixes |
| 索引没对齐就 concat(axis=1) | 出现大量 NaN | 先 reset_index 或按索引 merge |
8. 本章小结与练习
小结
concat:按轴拼接,axis=0纵向、axis=1横向,keys可标记来源;merge:SQL 式连接,on/left_on/right_on指定键,how选类型;join:按索引连接,是索引对齐的简便写法;- 连接类型:inner(交集)、left/right(保留一侧)、outer(并集);
- 实战套路:多张表按主键逐级 merge,配合
assign计算新列。
练习题
1. 将 1 月、2 月、3 月的三个 DataFrame 纵向拼接。
2. 用 merge 把订单表和商品表做左连接,计算订单金额。
3. 用 join 把两个索引一致的表连接起来。
4. 对比 inner / left / outer 三种连接的结果行数差异,并解释原因。
5. 合并两张都含"分数"列的表,用 suffixes 区分平时分和期末分。
下一篇预告:第 12 章 数据重塑与透视表 —— 掌握 pivot / pivot_table / melt / stack / unstack,让数据"站起来"和"躺下去"。
文章回复
0 条公开回复