员工离职预测实战:基于XGBoost的风险分层与归因分析
2026/9/16 1:55:58 网站建设 项目流程

员工离职预测与归因分析

去年年初,我接手了一个挺有意思的数据分析项目:用公司HR系统沉淀下来的员工信息,提前预测哪些人会在未来半年内有离职倾向。当时团队正面临一个很现实的问题——核心岗位人才流失率连续两个季度攀升,招聘成本居高不下,HRBP(人力资源业务伙伴)疲于奔命地做离职访谈,但往往是等人提了离职才反应过来,为时已晚。如果能提前锁定高风险人群,管理层就能在窗口期内做干预。这个项目就这么立项了。

整个分析过程用了大概三周时间,从数据清洗、特征工程、模型选型到最终的业务落地方案,踩了不少坑,也沉淀了一些值得分享的经验。这篇文章就把整个项目从0到1的过程完整梳理一遍,包括我当时的技术选型逻辑、关键代码实现、调参心得,以及最后怎么把模型预测结果转化成HR能直接用的行动清单。无论你是刚入门数据分析想找一个完整的实战案例,还是已经在做用户流失、员工流失相关分析的老手,这篇都应该能给你一些参考。

1. 项目整体设计与业务理解

1.1 离职预测到底是一个什么问题

从技术角度来说,员工离职预测是一个典型的二分类监督学习问题。我们要做的事情是:基于员工过去和当前的状态数据(包括年龄、司龄、薪资、绩效、考勤、加班时长等),训练一个模型,让它输出"未来某个时间段内离职/不离职"的概率。

但这里有一个关键点必须想清楚:离职预测不是算命,模型给的也不是一个绝对的"判决书"。它的本质是一个风险评分机制,帮你在海量员工中筛出值得关注的那批人,把有限的管理资源用在刀刃上。所以这个项目从一开始,我就没有把模型精度当成唯一KPI,而是更关注"预测结果能不能落到业务动作上"。

比如,模型告诉你张三有87%的离职概率,但如果你不知道他为什么可能离职,那这个数字对HRBP没有任何意义。所以这个项目实际包含了两条主线:一是预测(谁要走),二是归因(他为什么想走)。两条线配在一起,才能形成完整的决策闭环。

1.2 框架选型与工具链选择

工具链方面,我最后定了Python + Pandas + Scikit-learn + XGBoost这套组合,没有上Spark。原因很简单:数据量在十万级以内,单机处理完全没有压力,Spark在这个量级反而会增加不必要的分布式调度开销。这里想多说一句,很多初学者容易陷入"什么火用什么"的误区,但实际工作中,工具的选取从来都是根据数据量和业务场景来定的。

具体分工是这样:

  • Pandas负责数据清洗和特征工程,这个环节大概占了整个项目60%的工作量;
  • Matplotlib + Seaborn做探索性可视化,用来发现特征与离职之间的初步关系;
  • Scikit-learn提供基础的机器学习算法和模型评估工具;
  • XGBoost作为最终的生产模型,因为它在表格数据上的表现通常优于传统模型,并且自带特征重要性输出,方便我后面做归因分析。

建模环境用的是本地的 Jupyter Notebook,后期为了调参方便,也写了一部分脚本化代码。整个过程没有用任何付费工具,全部是开源组件。

1.3 模型指标的选取:为什么我用召回率而非准确率

分类模型的评估指标有很多,准确率、精确率、召回率、F1、AUC等等。在离职预测这个场景里,我需要特别强调一下指标选择的问题,因为这会直接影响你对模型好坏的判断。

我们的业务诉求是"尽可能找出会离职的人",而不是"对所有员工的判断都要均衡正确"。如果模型把10个真正会离职的人漏判了8个,即使它对另外900个留任员工的判断全部正确,准确率也有98.2%,但这个模型对于业务来说是废的——因为HR真正关心的那几个人一个都没找出来。

所以我当时把核心指标定在了召回率(Recall)上,同时在调参时兼顾精确率(Precision),用F1分数做一个平衡。如果只追求召回率,模型会把所有人都预测成"会离职",那也没有任何区分度。实际操作中,我要求模型在保持召回率不低于70%的前提下,尽量把精确率做高。后面会展示这一块的具体实现。

2. 数据探索与特征工程的完整拆解

2.1 数据结构说明与初步观察

我们拿到的原始数据是HR系统导出的一个宽表,一共10000条记录,每条记录代表一个员工在某个时间截面的状态快照。核心字段大概有这么几类:

  • 基本信息:年龄、性别、婚姻状况、教育程度、籍贯(是否本地)、部门、岗位
  • 组织信息:司龄、当前职级、汇报层级深度、最近一次晋升距今月份、是否有过转岗
  • 薪酬信息:月薪、最近一次调薪幅度(百分比)、薪资与同岗位中位数的比值
  • 考勤与绩效:过去12个月平均绩效评分、过去12个月加班时长总和、近3个月请假天数、是否有过违规记录
  • 目标标签:未来6个月内是否离职(1/0)

这里有一个很重要的事情,就是标签的定义。离职预测的标签不能简单地用"是否离职"一个布尔值,因为离职分为主动离职和被动离职(公司辞退、合同到期不续签等)。我们做离职预测,业务方真正关心的是主动离职,因为被动离职是公司可控的,不需要预防。所以我在清洗数据时,把被动离职的样本直接剔除了,凡是离职原因在合同到期、辞退、退休这几类的,都不进入建模。

清洗之后,正样本(主动离职)占比大概是16%,负样本(在职/被动离职剔除后留下)占比84%。这个比例属于中等不平衡状态,不需要特别复杂的采样策略,但会在模型训练时留意类别权重。

2.2 单变量分析:哪些因素和离职强相关

拿到数据之后,我习惯先不看模型,而是用最朴素的方式感受一下数据:每个特征单独拎出来,看它在"离职组"和"留任组"的分布差异。这一步虽然简单,但能让你对数据建立直觉,避免后面被模型的复杂逻辑带偏。

几个比较明显的发现:

司龄与离职率呈现明显的U型关系。入职不到1年的新员工离职率最高,达到22%左右;1到3年是相对稳定期;但司龄超过5年的老员工离职率又开始回升。这个现象其实很符合职业心理规律:新员工处于磨合期,容易因为预期落差、融入困难等原因离开;而老员工往往面临职业天花板、薪酬固化的瓶颈,一旦外部有机会,很容易被挖走。

调薪幅度是个非常关键的分水岭。过去12个月内没有调薪记录(或调薪幅度低于5%)的员工,离职率接近28%;而调薪幅度超过15%的员工,离职率只有约7%。这个差距在单变量分析里是最显著的,也直接印证了薪酬激励与留任之间的强关联。

加班时长与离职率不是简单的线性正相关。乍看之下,加班多的人离职率高一些,但细分后发现,中等加班时长(每月20-40小时)的员工离职率反而低于完全不加班的人。合理的解释是,完全不加班可能说明这个人已经被边缘化,或者工作量不饱和,缺乏成长感;而适度加班往往伴随高强度项目参与感,反而能增强组织黏性。但这个规律在加班超过60小时的组里被完全打破,离职率直线上升——这说明过载加班是个危险信号。

2.3 特征工程:从原始字段到有效特征的转换

原始数据有30多个字段,但直接丢给模型是不行的。特征工程要做的就是把这些原始信息转换成模型真正能利用的形态。我当时做了四类处理:

第一类:缺失值处理。数据里最头疼的是调薪幅度这个字段有接近15%的缺失。后来和HR确认,缺失代表该员工在统计周期内没有经历过调薪。所以我用0来填充,而不是用均值或中位数——因为"没有调薪"本身就是一个非常有价值的业务信号,它就意味着激励缺位。

第二类:连续变量分箱。年龄、司龄、薪资这些字段直接作为连续值输入也可以,但分箱之后对模型更友好,也便于后续解读。我用了Pandas的cut函数:年龄分成了25以下、25-30、30-35、35-40、40以上五档;司龄按照0-1年、1-3年、3-5年、5年以上分档;薪资则按分位数切成五档。

第三类:衍生特征。这是我觉得最有价值的一部分。比如"薪资竞争力指数",定义是员工当前薪资与他所在岗位、职级的中位数薪资的比值。这个指数比绝对值更有意义,因为月薪2万在高级工程师里可能是低薪,但在专员里已经是高薪了。类似的衍生特征还有:"晋升速度"(当前职级所需标准年限与实际司龄的比值)、"薪酬-绩效匹配度"(高绩效低涨幅就是危险信号)。

第四类:时间窗口类特征。离职倾向往往不是突然出现的,而是有一个酝酿期。所以我在特征里加入了"近3个月请假天数"、"近3个月登录HR系统的频次"这类时间窗口特征。这些数据看起来和离职没有直接关系,但实际操作中发现,准备离职的人在请假频率和系统活跃度上会有明显变化,这类特征对模型的贡献不容小觑。

2.4 相关性检验与多重共线性控制

做特征工程的同时,我顺手做了一下特征间的相关性检验。为什么要做这个?因为如果两个特征高度相关,比如"月薪"和"薪资竞争力指数",模型可能会给它们分配不稳定的权重,导致泛化能力下降。

相关性热力图看下来,有几个明显的抱团现象:加班时长和绩效评分正相关(0.42),司龄和职级正相关(0.55),这是符合直觉的。我的处理方法是:对于相关系数超过0.7的特征对,只保留与目标变量相关性更高的那个。经过一轮筛选,最终进入模型的特征数量是24个,这24个特征之间的两两相关系数都控制在0.6以内。

最后还要提一点,特征工程做完之后,一定要把所有特征的量纲统一一下,尤其是树模型虽然对特征尺度不敏感,但后续如果要做特征重要性对比,还是需要数值在同一量级上。我用的是Scikit-learn的StandardScaler做了标准化,但这个操作对决策树类模型不是必须的,只是统一习惯。

3. 建模过程中的关键决策与代码实现

3.1 数据集切分:必须按时间切,不能随机切

这是建模环节最容易被忽视但最重要的一步:数据集切分方式。

在员工离职预测这个场景里,我们是用过去某段时间的数据预测未来某段时间的结果,天然带有时间先后关系。如果你用随机抽样的方式把数据集分成训练集和测试集,那么训练集里可能包含未来时间点的样本,测试集里也可能包含过去时间点的样本,这会导致模型严重过拟合——因为模型"偷看"了未来的信息,测试集的评估结果会虚高,上线之后效果断崖式下跌。

我当时的做法是:以某个时间节点为界,比如用2019年1月到2020年6月的数据做训练集,用2020年7月到2020年12月的数据做测试集,完全模拟真实的预测场景。

# 按时间顺序切分数据,而不是随机切分 train = df[df['统计月份'] <= '2020-06-01'] test = df[df['统计月份'] > '2020-06-01'] X_train = train.drop(['target', '员工ID', '姓名'], axis=1) y_train = train['target'] X_test = test.drop(['target', '员工ID', '姓名'], axis=1) y_test = test['target'] print(f'训练集样本量: {X_train.shape[0]}, 正样本占比: {y_train.mean():.2%}') print(f'测试集样本量: {X_test.shape[0]}, 正样本占比: {y_test.mean():.2%}')

切完之后,训练集和测试集的时间分布完全不同,模型无法从测试集"偷师",评估结果才真实可信。

3.2 三类模型对比:逻辑回归、随机森林、XGBoost

建模我习惯先跑几个基线模型做对比,而不是一上来就调XGBoost。原因很简单,基线模型能帮你估算一个合理的性能"地板",如果XGBoost在基线基础上没有明显提升,那说明问题可能出在特征工程而不是算法上。

第一版基线用的是逻辑回归。逻辑回归的优点是可解释性强,每个特征对应一个权重,可以直接说"调薪幅度每增加一个百分点,离职概率下降多少"。逻辑回归的缺点也很明显,它假设特征与目标之间是线性关系,而员工离职这个问题里,很多关系是非线性的(比如司龄的U型效应),所以逻辑回归的初始AUC大概在0.73左右,F1只有0.41,效果一般。

第二版用的是随机森林。随机森林通过集成多棵决策树,能捕捉非线性关系,而且对异常值和缺失值都比较鲁棒。我把n_estimators设为300,max_depth设为8,跑出来的AUC提升到了0.85,F1也到了0.55。作为基线来说,这个成绩已经可以接受。

第三版是XGBoost,也是我最终采用的生产模型。同样的特征输入下,XGBoost的AUC达到了0.89,F1提升了到0.62。提升主要来自两点:XGBoost的正则化机制减少了过拟合,而且它对特征的单调性约束处理得更好。XGBoost的核心代码大致是这样:

import xgboost as xgb from sklearn.metrics import roc_auc_score, classification_report model = xgb.XGBClassifier( n_estimators=500, max_depth=5, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, scale_pos_weight=3, random_state=42 ) model.fit( X_train, y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=30, verbose=False ) y_prob = model.predict_proba(X_test)[:, 1] y_pred = (y_prob >= 0.3).astype(int) print(f'AUC: {roc_auc_score(y_test, y_prob):.3f}') print(classification_report(y_test, y_pred))

这里有几个参数值得单独说:

  • scale_pos_weight:因为正负样本不平衡(约16% vs 84%),我设置了这个参数为负样本数除以正样本数的比值(约5),但实际试下来3效果更好,因为过高的正样本权重会导致大量误报,HR那边看不过来;
  • max_depth:5层对于这个数据量够了,太深会过拟合;
  • learning_rate:用0.05配合500棵树,比直接用0.1配200棵树效果更稳定;
  • early_stopping_rounds:设置30轮早停,避免无效训练浪费时间。

3.3 阈值选择:让模型为业务服务

默认情况下,模型输出的概率大于0.5就预测为正类。但在我们的场景里,正类占比只有16%,直接套0.5阈值会导致召回率极低,大量真正的离职人员被漏掉。

阈值的选择本质上是在精确率和召回率之间做权衡。我把阈值从0.1到0.5每隔0.05跑了一遍,观察不同阈值下的精确率、召回率和F1值。最终选择0.3作为判定阈值,这时候召回率是0.74,精确率是0.48。

可能有人会问,精确率只有48%意味着什么?意思是模型预测为"高风险离职"的员工里,大约有一半的人最终没有离职。这在业务上是可以接受的,因为对HR来说,对48%的"假阳性"做一次离职访谈的成本很低,但换来的是74%的"真阳性"被及时发现。宁可错杀,不可漏网,这就是离职预测和很多其他预测场景的区别。

from sklearn.metrics import precision_recall_curve precisions, recalls, thresholds = precision_recall_curve(y_test, y_prob) for thr in [0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5]: idx = np.argmin(np.abs(thresholds - thr)) print(f'阈值: {thr:.2f} | 精确率: {precisions[idx]:.2f} | 召回率: {recalls[idx]:.2f}')

跑完这个循环,阈值和指标的关系会非常直观。这比空谈"阈值应该怎么选"更实际。

4. 模型评估、特征重要性与落地应用

4.1 模型效果评估与可解释性分析

模型训练完成之后,除了看AUC、精确率、召回率这些指标,我还做了一步非常重要的分析:特征重要性排序。XGBoost自带的feature_importances_可以输出每个特征的贡献度,但我要提醒一句,这个数值是相对的,不是绝对的,只能用来做优先级排序,不能直接解读成"特征A比特征B重要3倍"。

我们跑出来的重要性Top 5是,按贡献度排序:

  1. 调薪幅度(最近12个月)
  2. 薪资竞争力指数
  3. 司龄(分箱后)
  4. 近3个月请假天数
  5. 最近一次晋升距今月份

这个排序和最初的单变量分析结果高度一致,这说明模型学到的东西和业务直觉是吻合的,可以放心交付。反过来,如果特征重要性和业务直觉完全冲突,那就要回头检查数据质量或者特征工程是不是有bug。

为了更直观地展示特征与离职概率的关系,我还用SHAP(SHapley Additive exPlanations)库做了进一步的可解释性分析。SHAP比feature_importances更强大的一点是,它能告诉你每个特征"对预测结果的方向性影响"。比如司龄这个特征,SHAP值显示:司龄在1-3年的区间时,SHAP值为负(降低离职概率),而司龄5年以上时SHAP值为正(增加离职概率)。这个U型关系通过SHAP依赖图可以一眼看清。

4.2 风险分层:把预测结果变成一个可操作的名单

模型输出的是0到1的连续概率值,但业务方不可能对着一个概率值做决策,尤其是几千人的大团队,逐个看概率根本不现实。所以我把预测结果做成了风险分层

  • 高风险(概率≥0.7):立即干预名单,占全体员工的约5%
  • 中高风险(0.3-0.7):重点关注名单,占约12%
  • 中低风险(0.15-0.3):常规关注名单,占约20%
  • 低风险(<0.15):暂不干预,占约63%

这里的分层阈值不是拍脑袋定的,而是和HRBP团队开了两次会,结合他们的实际承载能力确定的。如果每位HRBP每个月需要完成10-15次员工沟通,那么高风险名单的人数就是他们的工作上限。

关键的一点是:风险分层名单必须附带每个员工的Top 3风险因素。比如张三的风险标签可能包括"调薪幅度不足5%""司龄超过5年""近3个月请假天数增加300%"。这样HRBP在约谈员工之前就已经有了谈话的切入点和假设,沟通效率会高很多。

4.3 落地效果与业务反馈

项目上线之后,我们做了一次为期6个月的跟踪验证。重点看两个指标:一是模型预测的准确度,二是干预效率。

反馈出来的结果让我挺欣慰的:在模型标记为高风险的员工中,实际离职率是43.6%,而全公司平均离职率只有16%左右,说明模型确实筛出了超高危人群。与此同时,高风险人群中一个比较有意思的现象是:离职概率高于0.85的员工,即使HR做了干预,最终离职的比例仍然很高。这批人大概率是已经拿到了外部offer,属于"板上钉钉"的流失,干预的意义不大。真正能被干预挽回的是概率在0.3到0.7这个区间的人,他们有离职倾向但还没有走到不可挽回的地步。

这个发现直接改变了HR的资源分配策略:高风险人群做"离职原因确认",中高风险人群做"留任影响"。前者是止损,后者才是投资。前者靠沟通确认事实,后者靠激励方案施加影响。

5. 踩坑记录与经验总结

5.1 数据层面的坑

这个项目最大的坑,我甚至愿意单独拎出来说:离职原因字段非常不可靠。

我从HR系统里拿到的离职原因五花八门,"个人原因""家庭原因""寻求更好发展"占了大多数。但做离职访谈的人都知道,员工填写的离职原因和真实的离职原因之间有巨大的鸿沟。如果真的有人因为"家庭原因"离职,他大概率不会在离职原因栏里写"因为钱给少了"。如果直接用这个字段做归因分析,结论会很荒谬。

我的应对办法是:不用离职原因字段做特征,只用它做标签(是否离职),归因分析完全依赖模型输出的特征重要性。也就是说,我让模型自己去发现"调薪幅度低"和"离职概率高"之间的关系,而不是人工告诉模型"员工说自己因为钱少离职"。这个方法规避了主观填报信息的偏差。

第二个坑是时间窗口不一致。HR系统的数据是月快照更新的,但不同模块的更新时间不完全同步。比如考勤数据可能是月初录入的,但薪资数据可能是月中更新的。如果不做时间对齐,模型会把"未来的薪资数据"和"过去的离职结果"混在一起,造成数据泄漏。我的处理方法是:统一以工资发放月份作为时间基准,其他所有特征都取这个时间点之前的数据,坚决不用"未来"的数据。

5.2 模型层面的坑

关于不平衡数据的处理,我也要多说几句。最开始我用SMOTE过采样把正负样本变成1:1,效果反而变差了。原因可能是SMOTE合成的样本并不符合真实的业务逻辑——员工离职不是一个线性插值的过程,两个高离职倾向员工的特征取平均值,不一定代表一个真实的潜在离职者。

后来改用scale_pos_weight处理类别不平衡,效果明显更好。XGBoost的scale_pos_weight本质上是给少数类样本的梯度加了权重,并不创造新样本,保留的真实模式更可靠。这也算是我做这类预测问题的一个经验:先尝试代价敏感学习,再做采样

5.3 业务落地层面的坑

最后是业务落地层面的教训。刚开始我交了一版技术报告,里面写满了AUC、F1、SHAP这些术语,业务方看得一头雾水。后来我意识到,数据分析的价值不在于模型多精确,而在于业务方能不能用起来。

现在做这个类型的项目,我会把交付物分成三层:

  • 管理层一页纸:核心结论和趋势,用可视化和人话写清楚,让决策者30秒抓住要点
  • HRBP操作手册:风险分层名单、谈话话术建议、干预措施清单
  • 技术附录:模型参数、评估指标、特征重要性,方便后续数据分析团队迭代

三层各取所需,不再用一份报告打所有人。

写在最后的两个小建议

结合这个项目,我想分享两个自己的体会,希望对正在做或者准备做离职预测的人有帮助。

第一,没有放之四海而皆准的模型。我们这个项目里XGBoost表现最好,换了另一个公司、另一种组织文化的数据,可能随机森林甚至逻辑回归更好。关键是建立一套完整的评估、对比、迭代框架,而不是执着于某一个算法。模型的本质是对业务理解的代码化表达,业务理解到位了,用什么模型都能出效果。

第二,离职预测只是起点,不是终点。真正让老板掏钱的是一个完整的闭环:预测出高风险人群,定位出风险因素,制定出干预措施,然后在下一次预测里去验证干预的效果。沿着这条路继续做,你甚至可以建立一个"留任实验"框架——对不同的人用不同的激励策略,用AB测试的思路验证哪种方式真正能留住人。这才是一个成熟的数据分析项目的价值所在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询