Python数据分析实战:用pd.merge()搞定员工工资表合并(附完整代码)
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_on和right_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_id和employee_id作为两个不同的列都保留了?这是因为我们指定了不同的左右键。但更多时候,当两个表有非连接键的同名列时,pd.merge()会自动添加后缀以区分。
假设我们的employees表和s表都有一个name列(虽然例子中s表没有),合并时就会产生name_x和name_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()支持传递一个列表给on、left_on、right_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()的性能需要关注。这里有几个小技巧:
-
合并前过滤:如果只需要合并部分数据,先使用布尔索引或
query方法过滤,可以显著减少数据量。# 只合并技术部的员工 tech_employees = employees[employees['department'] == '技术部'] merged_tech = pd.merge(tech_employees, salary, left_on='emp_id', right_on='employee_id', how='left') -
设置索引后合并:如果经常需要按某列合并,可以先将该列设置为索引,然后使用
left_index和right_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') -
关注
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()时,我总结了一些容易踩坑的地方和最佳实践:
-
键值重复:如果连接键在任一张表中有重复值,合并会产生笛卡尔积,导致数据行数爆炸式增长。合并前务必检查键的唯一性。
# 检查键的唯一性 if employees['emp_id'].duplicated().any(): print("警告:员工表中存在重复的emp_id!") -
数据类型不匹配:如前所述,确保连接键类型一致。日期、字符串、数字的混用是常见错误源。
-
合并后列名冲突:善用
suffixes参数,让列名清晰易懂。合并后,及时使用drop方法或列选择清理不必要的重复列。 -
内存问题:合并大型数据集非常消耗内存。如果内存不足,可以考虑:
- 使用
dask或modin等库进行分布式或核外计算。 - 分块读取和合并数据。
- 在合并前,只选择需要的列(使用
[['col1', 'col2']])。
- 使用
-
验证合并结果:合并后,使用
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()写几行代码,你会发现,数据整合原来可以如此优雅和高效。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)