RSS
菜单
全部文章快讯开发科技深度热点

第 11 章 数据合并:concat / merge / join

内容摘要

真实分析中,数据往往分散在多张表里:订单表、客户表、商品表……把它们合起来是必修课。本篇文章讲解 pandas 的三大合并武器:**concat(拼接)、merge(SQL 式连接)、join(索引连接)**。

真实分析中,数据往往分散在多张表里:订单表、客户表、商品表……把它们合起来是必修课。本篇文章讲解 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,让数据"站起来"和"躺下去"。
— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 11 章 数据合并:concat / merge / join》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。