Python数据分析实战:用pd.merge()搞定员工工资表合并(附完整代码)

每个月总有那么几天,财务部的李姐和人事部的王哥会对着电脑屏幕眉头紧锁。李姐手里有一份最新的工资发放明细,王哥那里则是一份员工基础信息表。他们需要把这两份数据合并起来,生成一份包含员工姓名、部门、岗位和应发工资的完整报表。过去,他们要么手动在Excel里用VLOOKUP,要么就是复制粘贴,不仅效率低下,还容易出错。直到他们开始接触Python和pandas,特别是那个神奇的pd.merge()函数,整个流程才变得清晰、高效且可靠。

如果你也经常需要处理来自不同部门、不同系统的数据表,并且为如何将它们准确、高效地关联起来而烦恼,那么掌握pd.merge()绝对是提升你数据处理能力的必修课。它不仅仅是Python pandas库中的一个函数,更是连接数据世界的桥梁,其灵活性和强大功能堪比SQL中的JOIN操作,但使用起来更加直观和便捷。本文将从一个真实的HR/财务数据处理场景出发,带你深入理解pd.merge()的核心用法、参数技巧以及那些容易踩的“坑”,并提供可直接运行的代码示例,让你看完就能上手解决实际问题。

1. 场景引入:当员工信息表遇上工资表

假设我们手头有两张表。第一张是employees表,记录了员工的基础信息。

import pandas as pd

# 创建员工基础信息表
employees = pd.DataFrame({
    'emp_id': ['E001', 'E002', 'E003', 'E004', 'E005'],
    'name': ['张三', '李四', '王五', '赵六', '钱七'],
    'department': ['技术部', '市场部', '技术部', '人事部', '财务部'],
    'hire_date': ['2021-03-15', '2020-08-22', '2022-01-10', '2019-11-05', '2023-05-30']
})
print("员工信息表:")
print(employees)

输出结果:

员工信息表:
  emp_id name department  hire_date
0   E001   张三       技术部  2021-03-15
1   E002   李四       市场部  2020-08-22
2   E003   王五       技术部  2022-01-10
3   E004   赵六       人事部  2019-11-05
4   E005   钱七       财务部  2023-05-30

第二张是salary表,记录了员工本月的工资明细。

# 创建本月工资表
salary = pd.DataFrame({
    'employee_id': ['E001', 'E002', 'E003', 'E006'], # 注意:E004和E005没有,但多了E006
    'base_salary': [15000, 12000, 18000, 9000],
    'bonus': [3000, 2000, 5000, 1000],
    'tax': [1800, 1200, 2500, 800]
})
print("\n工资表:")
print(salary)

输出结果:

工资表:
  employee_id  base_salary  bonus   tax
0        E001        15000   3000  1800
1        E002        12000   2000  1200
2        E003        18000   5000  2500
3        E006         9000   1000   800

现在,我们的目标很明确:将这两张表合并,得到一份完整的员工工资明细报告。理想情况下,报告应该包含所有员工的信息,即使某位员工在另一张表中没有记录,我们也希望能看到,并标记出缺失的数据。这听起来是不是很像数据库里的全外连接(FULL OUTER JOIN)?没错,pd.merge()正是为此而生。

2. 核心武器:pd.merge() 参数精讲与实战

pd.merge()函数的强大,在于它提供了丰富的参数来应对各种复杂的合并需求。我们先来看看它的基本语法:

pd.merge(left, right, how='inner', on=None, left_on=None, right_on=None,
         left_index=False, right_index=False, sort=True,
         suffixes=('_x', '_y'), copy=True, indicator=False)

看起来参数不少,别担心,我们结合上面的案例,把最关键的几个参数掰开揉碎了讲。

2.1 连接类型(how参数):决定数据的去留

how参数是pd.merge()的灵魂,它决定了合并后保留哪些数据。主要有四种选择:

连接类型 (how)SQL 等价操作描述适用场景
'inner'INNER JOIN默认值。只保留两个表中键值完全匹配的行。只需要双方都有的数据,确保结果集完整无缺失。
'left'LEFT OUTER JOIN保留左表的所有行,右表匹配不上的用NaN填充。以左表为基准,查看右表的补充信息。
'right'RIGHT OUTER JOIN保留右表的所有行,左表匹配不上的用NaN填充。以右表为基准,查看左表的补充信息。
'outer'FULL OUTER JOIN保留两个表的所有行,任何一方匹配不上的都用NaN填充。需要看到两个表的全貌,找出数据缺失情况。

回到我们的案例。如果我们想看到所有员工的信息,包括那些本月可能没有工资记录的(如新入职还未发薪的E005),或者工资表里有但员工表里没有的(如可能是已离职员工E006),就应该使用outer连接。

# 使用outer连接,查看所有数据
merged_outer = pd.merge(employees, salary, how='outer', left_on='emp_id', right_on='employee_id')
print("全外连接 (how='outer') 结果:")
print(merged_outer)

输出结果:

全外连接 (how='outer') 结果:
  emp_id name department  hire_date employee_id  base_salary  bonus   tax
0   E001   张三       技术部  2021-03-15        E001      15000.0 3000.0 1800.0
1   E002   李四       市场部  2020-08-22        E002      12000.0 2000.0 1200.0
2   E003   王五       技术部  2022-01-10        E003      18000.0 5000.0 2500.0
3   E004   赵六       人事部  2019-11-05         NaN          NaN    NaN    NaN
4   E005   钱七       财务部  2023-05-30         NaN          NaN    NaN    NaN
5   E006  NaN        NaN         NaN        E006       9000.0 1000.0  800.0

从结果可以清晰地看到:

  • E001, E002, E003在两表中都有,信息完整。
  • E004和E005在员工表中有,但工资表中没有,所以工资相关字段为NaN。这提示我们需要核查:是本月无需发薪(如请假),还是数据遗漏?
  • E006在工资表中有,但员工表中没有,基础信息为NaN。这很可能是一位已离职但还需结算工资的员工,或者是一个数据错误。

提示:在实际业务中,outer连接常用于数据质量检查和完整性验证,能快速定位出两张表的差异记录。

如果我们的需求是生成一份本月实际需要发放工资的员工清单,那么就应该使用inner连接,只保留双方都有的记录。

# 使用inner连接,只保留双方都有的员工
merged_inner = pd.merge(employees, salary, how='inner', left_on='emp_id', right_on='employee_id')
print("\n内连接 (how='inner') 结果:")
print(merged_inner)

输出结果:

内连接 (how='inner') 结果:
  emp_id name department  hire_date employee_id  base_salary  bonus   tax
0   E001   张三       技术部  2021-03-15        E001        15000   3000  1800
1   E002   李四       市场部  2020-08-22        E002        12000   2000  1200
2   E003   王五       技术部  2022-01-10        E003        18000   5000  2500

2.2 指定连接键(on, left_on, right_on):当列名不一致时

在上面的例子中,我们使用了left_on='emp_id'right_on='employee_id'。这是因为两个表中标识员工的列名不同。这是实际工作中非常常见的情况,不同系统导出的数据,对同一实体的命名往往不一致。

  • on参数:当两个表中用于连接的列名称完全相同时使用。例如,如果两张表都有employee_id列,可以直接写on='employee_id'
  • left_onright_on参数:当两个表中连接键的列名不同时使用。分别指定左表和右表的列名。

如果我们的salary表里,员工ID列也叫emp_id,那么合并就会简单很多:

# 假设salary表的ID列名也是'emp_id'
salary_renamed = salary.rename(columns={'employee_id': 'emp_id'})
merged_simple = pd.merge(employees, salary_renamed, on='emp_id', how='outer')
print("\n当连接键列名一致时,使用on参数:")
print(merged_simple[['emp_id', 'name', 'department', 'base_salary']].head())

2.3 处理重复列名(suffixes参数):让数据来源一目了然

你有没有注意到,在我们最初的outer连接结果中,连接键emp_idemployee_id作为两个不同的列都保留了?这是因为我们指定了不同的左右键。但更多时候,当两个表有非连接键的同名列时,pd.merge()会自动添加后缀以区分。

假设我们的employees表和s表都有一个name列(虽然例子中s表没有),合并时就会产生name_xname_y。我们可以通过suffixes参数自定义这些后缀,使其更具业务含义。

# 创建一个也有'name'列的salary表(仅用于演示)
salary_with_name = pd.DataFrame({
    'emp_id': ['E001', 'E002', 'E003'],
    'name': ['张三(财务系统)', '李四(财务系统)', '王五(财务系统)'], # 假设财务系统里的名字有后缀
    'salary': [15000, 12000, 18000]
})

# 合并,并自定义后缀
merged_with_suffix = pd.merge(employees, salary_with_name, on='emp_id', how='inner', suffixes=('_hr', '_finance'))
print("\n使用自定义后缀区分同名来源列:")
print(merged_with_suffix)

输出结果:

使用自定义后缀区分同名来源列:
  emp_id name_hr department  hire_date        name_finance  salary
0   E001     张三       技术部  2021-03-15  张三(财务系统)   15000
1   E002     李四       市场部  2020-08-22  李四(财务系统)   12000
2   E003     王五       技术部  2022-01-10  王五(财务系统)   18000

这样,我们一眼就能看出name_hr来自人力资源系统,name_finance来自财务系统,便于后续的数据核对与清洗。

3. 进阶技巧与实战陷阱规避

掌握了基本用法,我们来看看几个更贴近实战的进阶技巧和常见问题。

3.1 多键连接:更精确的匹配

有时候,仅凭一个ID不足以唯一确定一条记录。例如,工资表可能是按月记录的,我们需要根据员工ID月份两个字段来合并。pd.merge()支持传递一个列表给onleft_onright_on参数,实现多键连接。

# 创建带月份的工资表
salary_monthly = pd.DataFrame({
    'emp_id': ['E001', 'E001', 'E002', 'E002', 'E003'],
    'month': ['2024-01', '2024-02', '2024-01', '2024-02', '2024-01'],
    'salary': [15000, 15500, 12000, 12500, 18000] # 假设2月份涨薪了
})

# 创建带月份的员工部门调动表(假设员工部门会变动)
dept_history = pd.DataFrame({
    'emp_id': ['E001', 'E001', 'E002', 'E003'],
    'month': ['2024-01', '2024-02', '2024-01', '2024-01'],
    'department': ['技术部', '技术部(后端组)', '市场部', '技术部'] # E001在2月调整了部门细分
})

# 根据员工ID和月份进行合并
merged_multi_key = pd.merge(salary_monthly, dept_history, on=['emp_id', 'month'], how='left')
print("\n多键连接(按员工ID和月份):")
print(merged_multi_key)

输出结果:

多键连接(按员工ID和月份):
  emp_id    month  salary        department
0   E001  2024-01   15000             技术部
1   E001  2024-02   15500  技术部(后端组)
2   E002  2024-01   12000             市场部
3   E002  2024-02   12500               NaN
4   E003  2024-01   18000             技术部

这里,E002在2月份的部门信息是NaN,因为dept_history表中没有他2月份的记录。这真实地反映了数据现状。

3.2 合并后的数据清洗与计算

合并通常不是终点,而是数据处理的中间步骤。合并后,我们经常需要清洗数据、计算衍生字段。

# 接上例,计算应发工资(假设为基本工资)
merged_multi_key['应发工资'] = merged_multi_key['salary']

# 处理缺失的部门信息,用前一个月的部门信息填充(前向填充)
merged_multi_key['department'] = merged_multi_key.groupby('emp_id')['department'].ffill()
print("\n处理缺失部门信息后(前向填充):")
print(merged_multi_key)

# 或者,更常见的,为缺失值设置一个默认值
merged_multi_key['department'] = merged_multi_key['department'].fillna('部门待确认')
print("\n处理缺失部门信息后(填充默认值):")
print(merged_multi_key)

3.3 性能考量与大数据集处理

当处理非常大的数据集时,pd.merge()的性能需要关注。这里有几个小技巧:

  1. 合并前过滤:如果只需要合并部分数据,先使用布尔索引或query方法过滤,可以显著减少数据量。

    # 只合并技术部的员工
    tech_employees = employees[employees['department'] == '技术部']
    merged_tech = pd.merge(tech_employees, salary, left_on='emp_id', right_on='employee_id', how='left')
    
  2. 设置索引后合并:如果经常需要按某列合并,可以先将该列设置为索引,然后使用left_indexright_index参数。这在某些情况下(特别是索引已排序时)可能更快。

    employees_indexed = employees.set_index('emp_id')
    salary_indexed = salary.set_index('employee_id')
    merged_by_index = pd.merge(employees_indexed, salary_indexed, left_index=True, right_index=True, how='left')
    
  3. 关注dtype:确保连接键的数据类型一致。如果一个是字符串,另一个是整数,合并会失败或产生意外结果。使用astype()进行转换。

    employees['emp_id'] = employees['emp_id'].astype(str)
    salary['employee_id'] = salary['employee_id'].astype(str)
    

4. 综合案例:构建月度人力成本分析报表

让我们把所有知识串联起来,完成一个更复杂的实战任务:为管理层生成一份月度人力成本分析简报。

任务:结合员工信息、当月工资及考勤数据(假设有),计算各部门的工资总额、平均工资、出勤率等。

# 1. 准备数据
employees = pd.DataFrame({
    'emp_id': ['E001', 'E002', 'E003', 'E004', 'E005'],
    'name': ['张三', '李四', '王五', '赵六', '钱七'],
    'department': ['技术部', '市场部', '技术部', '人事部', '财务部'],
    'level': ['P7', 'P6', 'P6', 'P5', 'P5']
})

salary_jan = pd.DataFrame({
    'emp_id': ['E001', 'E002', 'E003', 'E004'],
    'month': ['2024-01'] * 4,
    'base': [30000, 25000, 22000, 18000],
    'bonus': [10000, 8000, 6000, 3000]
})

attendance = pd.DataFrame({
    'emp_id': ['E001', 'E002', 'E003', 'E004', 'E005'],
    'work_days': [22, 20, 23, 22, 21], # 应出勤天数
    'actual_days': [22, 18, 23, 20, 21] # 实际出勤天数
})

# 2. 分步合并
# 首先,合并工资和考勤
salary_attendance = pd.merge(salary_jan, attendance, on='emp_id', how='outer')
print("第一步:工资与考勤合并")
print(salary_attendance)

# 然后,将上一步结果与员工信息合并
full_data = pd.merge(employees, salary_attendance, on='emp_id', how='left')
print("\n第二步:与员工信息合并")
print(full_data)

# 3. 数据清洗与计算
# 填充缺失的工资数据为0(假设未发薪)
full_data[['base', 'bonus']] = full_data[['base', 'bonus']].fillna(0)
full_data['month'] = full_data['month'].fillna('2024-01')
full_data['total_salary'] = full_data['base'] + full_data['bonus']
full_data['attendance_rate'] = full_data['actual_days'] / full_data['work_days']
full_data['attendance_rate'] = full_data['attendance_rate'].fillna(0) # 处理可能除零或NaN的情况

print("\n第三步:计算衍生字段后")
print(full_data[['emp_id', 'name', 'department', 'total_salary', 'attendance_rate']])

# 4. 部门级聚合分析
dept_summary = full_data.groupby('department').agg(
    employee_count=('emp_id', 'count'),
    total_salary_sum=('total_salary', 'sum'),
    avg_salary=('total_salary', 'mean'),
    avg_attendance_rate=('attendance_rate', 'mean')
).round(2) # 保留两位小数

print("\n第四步:部门人力成本分析简报")
print(dept_summary)

输出结果(示例):

部门人力成本分析简报
        employee_count  total_salary_sum  avg_salary  avg_attendance_rate
department                                                                
人事部                1           21000.0     21000.0                 0.91
市场部                1           33000.0     33000.0                 0.90
技术部                2           58000.0     29000.0                 1.00
财务部                1               0.0         0.0                 1.00

通过这个流程,我们不仅完成了数据合并,还直接产出了一份有业务价值的分析报表。pd.merge()在这里扮演了数据枢纽的角色,将分散的数据源整合成了可供分析的数据集。

5. 避坑指南与最佳实践

在实际使用pd.merge()时,我总结了一些容易踩坑的地方和最佳实践:

  1. 键值重复:如果连接键在任一张表中有重复值,合并会产生笛卡尔积,导致数据行数爆炸式增长。合并前务必检查键的唯一性。

    # 检查键的唯一性
    if employees['emp_id'].duplicated().any():
        print("警告:员工表中存在重复的emp_id!")
    
  2. 数据类型不匹配:如前所述,确保连接键类型一致。日期、字符串、数字的混用是常见错误源。

  3. 合并后列名冲突:善用suffixes参数,让列名清晰易懂。合并后,及时使用drop方法或列选择清理不必要的重复列。

  4. 内存问题:合并大型数据集非常消耗内存。如果内存不足,可以考虑:

    • 使用daskmodin等库进行分布式或核外计算。
    • 分块读取和合并数据。
    • 在合并前,只选择需要的列(使用[['col1', 'col2']])。
  5. 验证合并结果:合并后,使用shape属性检查行数列数,使用head()tail()查看数据,使用isnull().sum()检查缺失值,确保合并结果符合预期。

    print(f"合并后数据形状: {full_data.shape}")
    print("\n各列缺失值数量:")
    print(full_data.isnull().sum())
    

pd.merge()是pandas中最强大、最常用的函数之一。从简单的两表关联,到复杂的多源数据整合,它都能胜任。理解其核心参数how, on, left_on/right_on的含义,并能在实际业务场景中灵活运用,是每个使用Python进行数据分析的人的必备技能。下次当你面对需要“VLOOKUP”的场景时,不妨试试用pd.merge()写几行代码,你会发现,数据整合原来可以如此优雅和高效。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐