Pandas 数据合并与关联实战指南 从基础 merge 到高级 join 技巧详解
在数据分析和处理的过程中,数据往往分散在不同的文件或表中。如何将这些数据有效地整合在一起,是每个数据分析师必须掌握的核心技能。Pandas 作为 Python 数据分析的利器,提供了强大且灵活的数据合并与关联功能。本指南将带你从基础的 merge 操作开始,逐步深入到 join 和 concat,并探讨高级技巧与性能优化,助你实战中游刃有余。
1. 数据合并的重要性与 Pandas 核心工具
在开始代码实战前,我们需要理解为什么数据合并如此重要。现实世界的数据通常是“非结构化”或“碎片化”的。例如,你可能有一份用户基本信息表,一份用户订单表,还有一份用户行为日志表。要分析用户的购买行为与年龄、性别的关系,你就必须将这些表关联起来。
Pandas 主要通过以下三个函数来处理数据合并:
pd.merge(): 类似于 SQL 中的 JOIN 操作,用于基于一个或多个键(Key)将两个 DataFrame 连接起来。DataFrame.join():merge的一种便捷封装,主要用于基于索引(Index)进行连接。pd.concat(): 用于沿特定轴将多个对象(Series 或 DataFrame)堆叠在一起,类似于 SQL 的 UNION 操作,但功能更强大。
2. 基础篇:pd.merge() 的核心用法
pd.merge() 是 Pandas 中最常用、功能最全的关联函数。它默认执行内连接(Inner Join),即只保留两个表中键都存在的行。
2.1 一对一连接 (One-to-One)
这是最简单的连接形式,假设我们有两张表,它们都有一个唯一的键,且该键在两张表中都存在。
import pandas as pd # 创建用户信息表 df_user = pd.DataFrame({ 'user_id': [1, 2, 3], 'name': ['Alice', 'Bob', 'Charlie'], 'age': [25, 30, 35] }) # 创建用户积分表 df_score = pd.DataFrame({ 'user_id': [1, 2, 3], 'points': [100, 200, 150] }) # 默认基于共同列(user_id)进行合并 merged_df = pd.merge(df_user, df_score) print(merged_df) 输出结果:
user_id name age points 0 1 Alice 25 100 1 2 Bob 30 200 2 3 Charlie 35 150 解析:Pandas 自动检测到了 user_id 是两个表共有的列,并以此作为连接键。
2.2 多对多连接 (Many-to-Many)
当连接键在两个表中都不是唯一时,就会发生多对多连接。Pandas 会自动进行笛卡尔积(Cartesian Product)操作。
# 左表:员工与部门(一个员工可能属于多个临时项目组) df_left = pd.DataFrame({ 'key': ['A', 'B', 'C'], 'data': [1, 2, 3] }) # 右表:项目组与预算(一个项目组可能有多个预算来源) df_right = pd.DataFrame({ 'key': ['B', 'B', 'D'], 'data': [4, 5, 6] }) # 多对多合并 result = pd.merge(df_left, df_right, on='key') print(result) 输出结果:
key data_x data_y 0 B 2 4 1 B 2 5 2 B 2 4 # 注意:如果左表有2个B,右表有2个B,结果会有4行 解析:左表中的 ‘B’ 对应右表中的两个 ‘B’,因此合并后产生了两行 ‘B’ 的数据。
2.3 处理重复列名
如果两个表有除连接键以外的同名列,Pandas 会自动添加后缀 _x 和 _y。当然,你也可以自定义后缀。
df1 = pd.DataFrame({'key': [1, 2], 'value': ['A', 'B']}) df2 = pd.DataFrame({'key': [1, 2], 'value': ['C', 'D']}) # 自定义后缀 result = pd.merge(df1, df2, on='key', suffixes=('_left', '_right')) print(result) 输出结果:
key value_left value_right 0 1 A C 1 2 B D 3. 进阶篇:指定连接键与连接方式
在实际业务中,数据往往不那么规整。连接键可能名称不同,或者数据类型不一致。这时我们需要更精细的控制。
3.1 指定连接键 (left_on, right_on)
如果左表的键叫 id,右表的键叫 user_id,我们需要显式指定。
df_a = pd.DataFrame({'id': [1, 2, 3], 'val': ['a', 'b', 'c']}) df_b = pd.DataFrame({'user_id': [1, 2, 4], 'val': ['d', 'e', 'f']}) # 指定左右表不同的连接键 result = pd.merge(df_a, df_b, left_on='id', right_on='user_id') print(result) 注意:合并后会同时保留 id 和 user_id 两列。通常我们会在合并后手动删除多余的一列。
3.2 连接方式 (how 参数)
这是 merge 最强大的地方,决定了保留哪些数据。
inner(默认): 仅保留两个表都有的键。left: 保留左表所有键,右表没有的填NaN。right: 保留右表所有键,左表没有的填NaN。outer: 保留两个表的所有键,缺失值填NaN(类似于 SQL 的 FULL OUTER JOIN)。
# 演示不同连接方式 left = pd.DataFrame({'key': ['A', 'B', 'C'], 'val': [1, 2, 3]}) right = pd.DataFrame({'key': ['B', 'C', 'D'], 'val': [4, 5, 6]}) print("--- Left Join ---") print(pd.merge(left, right, on='key', how='left')) print("n--- Outer Join ---") print(pd.merge(left, right, on='key', how='outer')) Left Join 输出:
key val_x val_y 0 A 1.0 NaN 1 B 2.0 4.0 2 C 3.0 5.0 (保留了左表的 A,右表缺失的值为 NaN)
3.3 索引作为连接键
有时候数据是基于索引关联的,而不是列。Pandas 提供了 left_index=True 或 right_index=True 参数。
df1 = pd.DataFrame({'age': [25, 30]}, index=['Alice', 'Bob']) df2 = pd.DataFrame({'city': ['NY', 'LA']}, index=['Alice', 'Charlie']) # 基于索引合并 result = df1.join(df2, how='outer') # join方法默认基于索引 # 或者使用 merge result_merge = pd.merge(df1, df2, left_index=True, right_index=True, how='outer') print(result_merge) 4. 高级篇:DataFrame.join 与 pd.concat
4.1 DataFrame.join 的便捷性
join 方法主要是为了方便索引对索引的合并。它默认是左连接(how='left'),这与 merge 默认的内连接不同。
# 场景:主表是用户数据,索引是 user_id users = pd.DataFrame({'name': ['Alice', 'Bob']}, index=[1, 2]) # 附加表是统计数据,索引也是 user_id stats = pd.DataFrame({'score': [90, 85]}, index=[1, 2]) # 简单的 join 操作 combined = users.join(stats) print(combined) 高级技巧:join 可以一次性合并多个表。
stats2 = pd.DataFrame({'level': ['High', 'Low']}, index=[1, 2]) # 一次性合并两个表 combined = users.join([stats, stats2]) print(combined) 4.2 pd.concat 的灵活堆叠
concat 不像 merge 那样基于列的值匹配,而是基于轴(Axis)进行拼接。
场景 A: 纵向堆叠 (Axis=0)
当你有分月的数据表,需要合并成年度表时使用。
df_jan = pd.DataFrame({'date': ['2023-01-01'], 'sales': [100]}) df_feb = pd.DataFrame({'date': ['2023-02-01'], 'sales': [120]}) # 纵向堆叠 year_df = pd.concat([df_jan, df_feb], ignore_index=True) print(year_df) 注意:ignore_index=True 会重置索引,否则会保留原来的索引导致重复。
场景 B: 横向合并 (Axis=1)
类似于 join,但 concat 更底层。
df1 = pd.DataFrame({'A': [1, 2]}, index=[0, 1]) df2 = pd.DataFrame({'B': [3, 4]}, index=[0, 1]) # 横向合并 result = pd.concat([df1, df2], axis=1) 4.3 索引重置与层级处理
合并数据后,索引往往会变得混乱。掌握 reset_index 是必须的。
# 假设合并后的数据索引不连续 df = pd.DataFrame({'data': [1, 2, 3]}, index=[10, 20, 30]) # 重置索引,将旧索引变为列,生成新索引 df_reset = df.reset_index(drop=False) # drop=True 会丢弃旧索引 print(df_reset) 5. 实战案例:电商数据分析
让我们通过一个完整的案例,整合上述所有知识点。
需求:我们有三张表。
users.csv: 用户ID,姓名,注册城市。orders.csv: 订单ID,用户ID,订单金额,日期。products.csv: 产品ID,产品名称,类别。
目标:生成一张宽表,包含订单详情、用户信息以及产品信息。
import pandas as pd import numpy as np # 模拟数据 users = pd.DataFrame({ 'user_id': [101, 102, 103], 'name': ['张三', '李四', '王五'], 'city': ['北京', '上海', '深圳'] }) orders = pd.DataFrame({ 'order_id': [1, 2, 3, 4], 'user_id': [101, 102, 101, 104], # 注意:104是不存在的用户 'product_id': [1001, 1002, 1001, 1003], 'amount': [500, 300, 500, 200] }) products = pd.DataFrame({ 'product_id': [1001, 1002], 'product_name': ['iPhone', 'MacBook'], 'category': ['Electronics', 'Electronics'] }) # --- 第一步:订单与用户关联 (Left Join) --- # 我们想保留所有订单,即使用户信息缺失(数据清洗时发现问题) order_user = pd.merge(orders, users, on='user_id', how='left') # --- 第二步:关联产品信息 (Inner Join) --- # 假设只分析已知产品的订单 final_df = pd.merge(order_user, products, on='product_id', how='inner') # --- 第三步:数据清洗与计算 --- # 计算客单价(AOV),处理缺失值 final_df['unit_price'] = final_df['amount'] / 2 # 假设逻辑 # 检查是否有缺失的用户或产品信息 print("缺失值统计:") print(final_df.isnull().sum()) # --- 结果展示 --- print("n最终分析表:") print(final_df[['order_id', 'name', 'city', 'product_name', 'amount']]) 代码解析:
- 我们先用
leftjoin 连接用户表,这样如果orders中有非法的user_id(如 104),在name列会显示NaN,方便我们后续排查。 - 再用
innerjoin 连接产品表,因为如果产品信息缺失,该订单可能无法进行分类分析。 - 最终我们得到了一个包含所有维度的分析表。
6. 性能优化与常见陷阱
6.1 性能优化技巧
- 预过滤数据:在合并前,先使用
df[df['col'] > 100]减少数据量。 - 使用
set_index:如果基于列合并,且该列是唯一的,先将其设为索引再合并通常会比直接合并快。 - 数据类型优化:确保连接键的数据类型一致(例如都是
int32而不是object或str),这能显著提升合并速度。
6.2 常见陷阱
- 数据类型不匹配:
df1['id']是int,df2['id']是str,导致合并结果为空。解决:使用astype统一类型。 - 隐式笛卡尔积:忘记指定
on参数,且两个表有大量同名列,导致产生大量无意义的列和行。 - 内存溢出:两个巨大的表进行
outerjoin 可能导致内存爆炸。解决:分块处理(Chunking)或使用 Dask 等分布式计算库。
7. 总结
数据合并是数据分析的基石。掌握 pd.merge 的 how 参数和键指定方式,能解决 90% 的关联需求;熟练运用 join 可以简化索引操作;而 concat 则是处理多源数据堆叠的利器。
在实战中,始终遵循以下步骤:
- 明确分析目标。
- 检查数据质量(重复值、数据类型)。
- 选择合适的连接方式(通常先 Left Join 保留主表完整性)。
- 验证结果(检查行数是否符合预期,是否有意外的 NaN)。
支付宝扫一扫
微信扫一扫