☰
员工流失预测系统实战:特征工程、模型评估与落地
2026/9/29 7:02:51 网站建设 项目流程

简介:这套源码围绕人员流失预测与数据挖掘分析设计,面向企业数据分析人员、人力资源管理者及机器学习入门开发者,旨在通过历史员工数据识别离职关键因素,为降低流失率提供决策依据。资源压缩包内共有七十四份文件,整体大小约十点七兆字节,主要包含Python脚本、JavaScript脚本、CSS样式、HTML页面、XML配置、pickle模型结果以及多张数据可视化图片。其中Python脚本用于数据预处理、特征选择与模型训练,JavaScript与CSS/HTML负责前端交互和页面展示,XML文件用来定义运行参数,pickle文件则保存了决策树、K近邻、支持向量机、随机森林等多种算法的训练结果。该资源已有二百五十四人学习下载。借助这套源码,读者可以完整走通从数据清洗、特征工程、多算法建模到结果可视化的全流程,了解Django框架下系统模块的组织方式,还能直接利用保存好的模型文件对比不同算法效果,并针对自身企业数据快速修改与复用,搭建一套可用的员工流失预警与决策支持工具。

1. 人员流失预测与数据挖掘分析系统:先搞清楚这个系统到底在解决什么问题

同期入职的两个算法工程师,一个把流失预测模型的AUC跑到了0.93,另一个只做到0.86。半年后,前者写的模型脚本躺在仓库里没人再看,后者的预测名单被HR部门当作月度绩效访谈的依据。差别不在学过,而在后者把数据挖掘分析做成了系统:目标变量定义清楚、特征工程能解释、模型输出直接落到风险名单上。人员流失预测与数据挖掘分析系统,就是这类工具——它面对的不是论文数据集,而是一张真实的员工表。它要解决三类问题:哪些人未来半年可能离职、为什么、以及下一步该找谁聊。适合正在做HR数据分析、或想用Python给组织部门提效的从业者,前提是你能拿到带离职标记的历史员工数据。这篇笔记,我从特征工程讲到模型评估,再讲到系统落地和避坑,照着做能跑出一版可用的源码方案。

2. 从HR表到训练集:人员流失预测的数据清洗与特征工程

2.1 流失预测的本质:先决定你是在做分类还是做风险排序

人员流失预测在机器学习层面是标准的二分类问题:员工在某个时间窗口内是否离职。但业务上要的往往不是“会/不会”的标签,而是风险排序——下个月最可能离职的Top 50人是谁。这两个目标对应完全不同的评估方式和阈值策略,搞混了后面全白做。

常用的是公开的员工流失样例数据,通常是几千行带离职标记的结构化表格,常见字段包括年龄、月收入、岗位级别、满意度打分、加班标记、入职年限、上次晋升间隔等。这类数据挖掘项目的第一件事不是建模,而是给目标变量做定义:标记为1的是“已离职”的人,0是“在职”的人。看起来简单,但真实项目里最阴险的问题藏在时间上——离职面谈评分、离职当月的绩效考核、最后半年调薪记录,这些字段在预测时点根本拿不到,放进去就是数据泄漏。

所以资深做法是设计“观察期—表现期”窗口:用观察期(比如过去6个月)里能够稳定采集到的特征,去预测表现期内是否离职。没有时间列的静态截面数据只能做基线模型,上线后定期重建,这是后话。

提示:如果数据只有某一年截面的员工状态加离职标记,没有时间列,模型只能做静态预测。真实生产环境里这种模型过期很快,需要每个月重新训练。

2.2 用pandas把原始员工表清洗成建模样本

原始数据最常见的三种问题:离职标记是文本“Yes/No”、有整列缺失、存在员工编号重复。先写一个数据体检脚本。

import pandas as pd import numpy as np df = pd.read_csv("hr_attrition.csv", encoding="utf-8") print(df.shape) print(df.info()) # 目标变量转成 0/1,流失记为1 df["target"] = df["Attrition"].map({"Yes": 1, "No": 0}) print(df["target"].value_counts()) # 删除完全没数值变化的列,比如员工编号、无意义标志位 nunique = df.nunique() constant_cols = nunique[nunique <= 1].index.tolist() df = df.drop(columns=constant_cols, errors="ignore") # 数值列中位数填充,分类列众数填充 num_cols = df.select_dtypes(include=[np.number]).columns.tolist() cat_cols = df.select_dtypes(include=["object"]).columns.tolist() df[num_cols] = df[num_cols].fillna(df[num_cols].median()) df[cat_cols] = df[cat_cols].fillna(df[cat_cols].mode().iloc[0])

value_counts用来确认正负样本比例,流失率在很多企业里就是10%~20%的量级,这个比例决定了后续要不要处理不平衡。nunique() <= 1会把只有单一值的列全部筛掉,像员工编号这种每一行都不同的字段不会触发这个条件,因为它nunique很大,所以真正的坑在另一种情况:模型拿ID当特征,训练时AUC虚高、上线后彻底失效,后面避坑章节会专门讲。

缺失值填中位数和众数是保守做法,胜在稳定可复现。如果样本够多,还可以给每条缺失记录补一个“是否缺失”的标记列,给树模型去挖掘缺失模式。第一次做这个系统不建议引入太复杂的填充逻辑,先把数据跑通。

2.3 特征工程:五个在流失预测里几乎必用的维度

流失预测特征可以归成五类,这五个维度在公开样例和内部HR系统里基本都存在,按这个框架整理不会漏。

维度代表字段说明
满意度JobSatisfaction、EnvironmentSatisfaction、WorkLifeBalance有序分类,直接做序数编码
付出与回报MonthlyIncome、OverTime、StockOptionLevel相对值比绝对值更有意义
稳定性YearsAtCompany、YearsInCurrentRole、JobLevel构成员工在职时间画像
晋升YearsSinceLastPromotion长期不晋升是强信号
工作负荷TotalWorkingYears、PercentSalaryHike需警惕与离职目标的时间顺序

有序分类列如满意度,直接映射成数值1-4,不要做one-hot,否则序数信息被拆散成多个哑变量,模型反而学不到“满意度越低风险越高”的单调关系。无序分类列如部门、岗位,保持字符串交给管道里的OneHotEncoder处理。

派生特征我通常会做两个:晋升等待比和相对收入差。这两个特征带业务语义,比单纯堆原始字段更能得到可解释的预测结果。

# 晋升等待比:上次晋升间隔 占 总工龄 的比例 df["promo_wait_ratio"] = df["YearsSinceLastPromotion"] / (df["YearsAtCompany"] + 1e-3) # 相对收入:个人月收入 减去 同岗位平均月收入 df["income_gap"] = df["MonthlyIncome"] - df.groupby("JobRole")["MonthlyIncome"].transform("mean")

加1e-3是为了防止工龄为0时除零报错。收入差用的是岗位均值,如果你手里的数据有部门、职级两级结构,建议先按“部门+职级”分组再算均值,这个分组口径比单纯岗位更贴近员工真实感知的公平性。这里JobRole是字段名,内部数据换了名字就在这一行改。

这些清洗和特征逻辑写到后面会越来越长,所以第一版就要把预处理封成一个函数,所有实验都从同一入口过数据。改特征时只改一处,模型训练、名单导出、月度报告全部复用同一个预处理版本,这是整个源码工程后期能不能维护住的关键。

3. 流失预测建模:从基线模型到调参评估的完整可跑代码

3.1 为什么先跑逻辑回归?它和树模型一样值得认真对待

HR流失数据大部分是几千行的小样本,特征是表格结构。这种数据上,逻辑回归和随机森林是绕不开的两个模型,LightGBM排第三。逻辑回归的优点是线性可解释、训练快、系数天然能给业务讲权重;缺点是学不到非线性交互,需要手工构造交叉特征。随机森林不需要归一化就能抓到非线性关系,但它是黑匣子,特征重要性还有数值型变量偏高的毛病。

模型优点缺点适用场景
逻辑回归解释强、稳定、快需手工加交互项基线模型、制度效果评估
随机森林非线性、不用归一化黑匣子、重要性有偏第一版主力模型
LightGBM准确率高、缺值友好数据少时极易过拟合数据大于5000行且特征稳定

我的习惯是先跑逻辑回归作为基线,几乎不调参,确认数据管道没毛病之后跑随机森林。LightGBM留到最后,在HR这种千级样本上它很容易在验证集上表现惊艳、上线就翻车,只做参考对比。

3.2 一次训练两个模型:标准化、交叉验证和评估指标一起跑

用sklearn的Pipeline把预处理和模型包在一起,避免在训练测试拆分时泄漏预处理参数。这里有一个容易错的地方:StandardScaler必须在训练集上fit,再transform验证集和测试集,直接对全量数据fit再拆分,验证集就脏了。Pipeline能强制保证这个顺序。

import numpy as np from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.preprocessing import OneHotEncoder, StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split, cross_val_score from sklearn.metrics import roc_auc_score, precision_recall_curve # 预处理统一入口 X = df.drop(columns=["Attrition", "target"]) y = df["target"] num_cols = X.select_dtypes(include=[np.number]).columns.tolist() cat_cols = X.select_dtypes(include=["object"]).columns.tolist() preprocessor = ColumnTransformer([ ("num", StandardScaler(), num_cols), ("cat", OneHotEncoder(handle_unknown="ignore"), cat_cols), ]) models = { "lr": LogisticRegression(max_iter=1000, class_weight="balanced", random_state=42), "rf": RandomForestClassifier(n_estimators=300, min_samples_leaf=10, class_weight="balanced", random_state=42), } X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) probas = {} for name, model in models.items(): pipe = Pipeline([("prep", preprocessor), ("clf", model)]) pipe.fit(X_train, y_train) probas[name] = pipe.predict_proba(X_test)[:, 1] auc = roc_auc_score(y_test, probas[name]) print(f"{name} AUC: {auc:.3f}")

这里的两个模型都用了class_weight="balanced",因为流失样本天然少,不处理的话模型会把所有人判成在职,准确率照样能到85%。handle_unknown="ignore"是OneHotEncoder的必备参数,不设置的话线上预测出现新部门直接报错。随机森林的min_samples_leaf=10是防止单棵树过深过拟合,HR数据量小,叶子节点设10比较稳。

注意循环里的probas字典保存了两个模型的预测概率,后面阈值搜索要分别用。如果不存字典只在循环里打印AUC,后面想要逻辑回归的概率就得重训一遍,费时间。

3.3 指标看哪几个:准确率最没用,召回率与命中率的平衡

流失预测里准确率是个陷阱。流失率15%的数据集,把所有员工判成“不流失”,准确率85%,但这个模型没有任何决策价值。业务真正盯的是:给出的高风险名单里,到底有多少人真的走了。

所以核心指标只有三个:AUC、Top名单命中率、以及设定阈值后的召回率。其中AUC评估排序能力,命中率和召回率评估名单质量。下面的代码从概率里找能用的阈值。

# 以随机森林的预测概率为例,选择业务可用的阈值 precisions, recalls, thresholds = precision_recall_curve(y_test, probas["rf"]) # 目标:名单召回率至少60%,同时precision尽量高 for t, p, r in zip(thresholds, precisions[:-1], recalls[:-1]): if r >= 0.6 and p >= 0.3: print(f"阈值 {t:.3f} -> precision {p:.3f}, recall {r:.3f}") break

阈值搜索的定价是两个业务约束:召回率决定了系统能发现多少真实流失者, precision决定了HR拿到名单里有多少是误报。HR每周能访谈的名单是有限的,如果名单里十个人只走了一个,业务就不会再用。常见做法是和业务确定“召回不低于60%、命中率不低于30%”一类的目标,再回代找阈值。这里用测试集选阈值在严格流程上是偷懒的,正确做法是单独切验证集选阈值、测试集只做最终评测,数据量小时可先按这个能跑的版本做。

3.4 LightGBM要不要上:数据量说了算

如果手头数据超过5000行、字段稳定,可以加一个LightGBM做对照。注意要在交叉验证里看它的稳定性,单次切分的结果容易骗人。

from sklearn.ensemble import HistGradientBoostingClassifier hgb = HistGradientBoostingClassifier( max_iter=200, learning_rate=0.05, max_leaf_nodes=15, class_weight="balanced", random_state=42, ) pipe_hgb = Pipeline([("prep", preprocessor), ("clf", hgb)]) cv_auc = cross_val_score(pipe_hgb, X_train, y_train, cv=5, scoring="roc_auc") print(f"HGB CV AUC: {cv_auc.mean():.3f} ± {cv_auc.std():.3f}")

我这里选HistGradientBoostingClassifier而不是LightGBM原库,因为sklearn接口不需要额外装包,样本少时更不容易过拟合。max_leaf_nodes=15严格限制树复杂度,learning_rate=0.05配合max_iter=200做慢速学习。交叉验证的标准差如果超过0.03,说明模型对数据划分很敏感,不建议上线。

4. 数据挖掘分析:把预测黑匣子变成业务能看懂的几张图

4.1 用KMeans给员工群体分群:哪类小群人流失风险最高

模型给的是概率,业务需要的是认知。常见做法是对核心特征做KMeans聚类,把员工分成几个群体,再交叉模型预测概率看哪个人群风险最高。这才能回答“哪种员工最容易流失”而不是“这个人概率多少”。

from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler # 只选取带业务解释的连续特征参与聚类 cluster_features = ["JobSatisfaction", "MonthlyIncome", "YearsAtCompany", "WorkLifeBalance"] X_cluster = df[cluster_features].copy() scaled = StandardScaler().fit_transform(X_cluster) kmeans = KMeans(n_clusters=4, n_init=10, random_state=42) df["cluster"] = kmeans.fit_predict(scaled) # 每个簇的成员数、平均流失率、平均满意度 for c in sorted(df["cluster"].unique()): sub = df[df["cluster"] == c] print(c, "人数", len(sub), "流失率", round(sub["target"].mean(), 3), "满意度", round(sub["JobSatisfaction"].mean(), 2))

KMeans的标准化必须做,否则MonthlyIncome几千的数值会主导距离计算,满意度1到4的量纲直接被淹没。簇数量不是越多越好,一般先试3到5个,看每个簇是否能在业务上概括成一句话,比如“高收入、高工龄、满意度低的老员工”“低收入、高加班、频繁换岗的新人”。概括不出来的簇说明特征选得不对或者k值不合理。n_init=10是sklearn新版本的默认行为,显式写出来是为了防止旧版本陷入局部最优。

跑完聚类后,把每个簇的流失率和全公司基线比。如果某个簇流失率是整体的2倍,这就是分析系统最该输出的洞察,比单条预测概率更有决策价值。

4.2 特征重要性:随机森林的importances和逻辑回归系数要分开看

随机森林的feature_importances和逻辑回归的系数是两套逻辑。树模型的重要性偏向取值跨度大的数值特征,而逻辑回归系数反映的是单位边际影响。两者都排在前面的特征才是真正值得写进报告的东西。

# 取出已训练的随机森林管道 rf_pipe = models["rf"] importances = pd.Series( rf_pipe.named_steps["clf"].feature_importances_, index=rf_pipe.named_steps["prep"].get_feature_names_out() ).sort_values(ascending=False).head(10) print(importances)

用Pipeline训练后取特征名要调用get_feature_names_out(),这个方法返回的列名里OneHot编码的字段会带后缀,比如“OverTime_Yes”“OverTime_No”,直接看会很碎。我的习惯是先按原始字段名聚合,把所有以“OverTime”开头的列加总,再和逻辑回归的系数绝对值排序放在同一张表里对比。

# 逻辑回归系数的绝对值排序 lr_pipe = models["lr"] coef_abs = pd.Series( np.abs(lr_pipe.named_steps["clf"].coef_[0]), index=lr_pipe.named_steps["prep"].get_feature_names_out() ).sort_values(ascending=False).head(10) # 两列对比:取两边Top10的并集 top_feats = set(importances.head(10).index) | set(coef_abs.head(10).index) print(top_feats)

两边同时上榜的特征说明既有区分度又稳定,可以直接给业务写“建议重点监测”。只在一侧上榜的特征要小心解释:树上重要可能是数值分布造成的假象,逻辑回归系数大可能是多重共线性放大——所以两个方法交叉印证,就是数据分析这块最朴素也最有效的技巧。

注意:不要用feature_importances去谈因果,它只反映预测贡献,不反映干预效果。这点在后面避坑章节里还会强调。

4.3 输出风险名单与画像:从模型到分析系统的最后一步

模型的价值最终体现在名单上。把全量在职员工的预测概率落成带风险等级的表,导出成Excel可打开的csv,这是分析系统每天要被业务方看到的东西。

result = X_test.copy() result["prob"] = probas["rf"] # 用分位数定档,不用固定阈值 low, high = result["prob"].quantile([0.6, 0.85]) result["risk_level"] = pd.cut( result["prob"], bins=[0, low, high, 1], labels=["低", "中", "高"] ) result = result.sort_values("prob", ascending=False) # 按部门和风险等级汇总 pivot = result.groupby(["Department", "risk_level"], observed=False).size().unstack() print(pivot) result.to_csv("attrition_risk_list.csv", index=False, encoding="utf-8-sig")

编码用utf-8-sig,是让生成的csv在Excel直接打开不乱码,这是中文数据落地的老经验。风险等级的分位线每次训练完都要重新计算——样本分布变了,固定阈值0.8很可能一个高危险名单都出不来,用分位数能保证每次输出名单的规模稳定,符合业务对“每月固定访谈多少人”的预期。

整套源码工程做到这里,一般就可以拆成四个独立模块:数据预处理、模型训练、名单预测、报告导出。每个模块一个脚本,输入输出都是标准csv,互不耦合。这种结构看起来简单,但实际维护比一个大而全的notebook省心得多,因为HR数据每个月都在变,只重跑数据预处理到名单预测这一段就够了。

5. 流失预测系统的5个常见坑:现象、原因与解决办法

坑1:测试集AUC很高,名单却没人用

现象:模型在测试集上AUC达到0.9以上,但输出给HR的名单里,访谈命中的人寥寥无几。

原因:目标泄漏。最常见是把离职当年的绩效评分、离职访谈记录、当年调薪幅度当成了特征。这些信息在预测时点根本不存在——你不可能在1月份预测6月离职时,用上6月才会填写的绩效数据。

解决:做特征时逐个字段确认“这个值在预测截止日那天是否已经产生”。拿不准的字段宁可删掉。粗筛规则是:凡是带“离职”“访谈”“当年”字样的字段一律不给模型用,只保留观察期截止日之前能稳定采集到的信息。

坑2:模型把所有员工都判成“不流失”

现象:训练的模型预测概率全部低于阈值,输出的高风险名单是空的,或者名单里全是同一类边缘案例。

原因:典型的不平衡问题。流失率只有10%左右,模型发现全部判负能拿到很高的准确率,于是放弃了正样本。

解决:两个手段同时用。模型侧加class_weight="balanced",数据处理侧做阈值搜索而不是用默认0.5做切分。默认0.5是基于正负样本均衡的假设,在流失预测里完全不适用,必须用第3.3节的方法按业务目标找阈值。

坑3:过采样后验证指标虚高,上线回测直接翻车

现象:用SMOTE把训练集平衡后,测试集AUC大幅上升,但上线后实际命中率远低于预期。

原因:SMOTE被错误地应用到了全量数据上。过采样生成的合成样本和真实样本在特征空间高度重合,如果先过采样再切分训练测试集,合成样本会同时出现在两边,验证结果严重失真。

解决:过采样只能放在每一折交叉验证内部,只对训练折做过采样,验证折保持原始分布。如果工程上不好实现,干脆不用SMOTE,直接用class_weight加阈值搜索,效果差距远没有想象中大。

坑4:模型上线第二个月开始失效

现象:第一个月预测名单命中率尚可,第二个月明显下滑,第三个月基本没法用。

原因:特征分布漂移。公司招聘策略变了、晋升通道调整了、或者某个部门大规模重组,员工特征的整体分布跟着变,旧模型学到的规律过期了。

解决:每月出名单时,同时计算当月特征均值与训练集特征均值的差异,可以简单看PSI(群体稳定性指数)。PSI超过0.25就触发重训。如果HR系统每个月自动更新数据,就把训练脚本挂成月度任务,每次用最近12个月的数据重训一次,不要抱着一个模型跑半年。

坑5:把“相关”说成“因果”,报告被管理层挑战

现象:分析报告写“加班多的员工离职风险高2倍,建议禁止加班”,结果业务部门反弹,因为真正的原因可能是项目压力和团队管理,而不是加班本身。

原因:数据挖掘发现的是相关性,不是因果。加班和离职可能同时受第三个变量影响,比如项目烂、管理者风格差。

解决:报告里只写“高风险人群的特征是加班多、满意度低、长期未晋升”,要落实到管理动作必须做访谈验证或准实验设计。分析系统输出的是线索,不是结论。给业务建议时用“建议优先调研”而不是“必须禁止”。

6. 让分析系统可验证、可迭代:一份月度流失预测报告怎么落地

模型做出来只是上半段,分析系统的下半段是循环:每月出名单、记录预测时间、三个月后回测命中率、修正特征和参数。系统价值不在预测概率有多准,而在“预测排名前20%的人,占了真实流失人数的多少”——这个数字必须被度量,否则整个系统就只是自我感动。

一份可落地的月度报告至少包含四个sheet:风险名单、部门分布、特征对比、上期回测。最小实现用pandas就可以完成。

import pandas as pd with pd.ExcelWriter("monthly_report.xlsx") as writer: result.to_excel(writer, sheet_name="风险名单", index=False) pivot.to_excel(writer, sheet_name="部门分布") # 上期名单回测:预测Top20%与实际离职名单的交集 last_ids = pd.read_csv("last_month_risk_list.csv")["EmployeeID"] actual_left = df[df["target"] == 1]["EmployeeID"] top_n = int(len(last_ids) * 0.2) hit_rate = len(set(last_ids[:top_n]) & set(actual_left)) / top_n coverage = len(set(last_ids[:top_n]) & set(actual_left)) / len(actual_left) print(f"Top20%名单命中率: {hit_rate:.2f}, 覆盖真实流失: {coverage:.2f}")

回测时最好按排名切名单,而不是按风险等级。比如取上次预测概率最高的20%作为Top名单,再看这20%里真实离职的人占全部离职的百分比。这种做法能稳定回答“模型排序到底有没有用”,HR也能通过这个数字判断要不要继续信任这套分析系统。

我这些年做类似系统的习惯是:每次模型迭代都留下一个带日期的预测文件,哪怕模型没变化也照常存一份。三到五个月后,回测脚本把这些历史名单和真实离职数据一匹配,哪个版本的模型真正有效就一目了然。没有这份回测记录,所有的调参和特征工程都是玄学,业务不会再相信第二版。

预测名单本身不产生价值,名单变成访谈动作,再被回测验证,整个闭环才成立。每月做一次回测,比把AUC往上抠0.01重要得多。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询